#!/usr/bin/env python
# txt2tagslite - a modular txt2tags proof of concept
# http://txt2tags.org
#
# Copyright 2001-2013 Aurelio Jargas
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# License: http://www.gnu.org/licenses/gpl-2.0.txt
# Subversion: http://svn.txt2tags.org
# Bug tracker: http://bugs.txt2tags.org
#
########################################################################
#
#   BORING CODE EXPLANATION AHEAD
#
# Just read it if you wish to understand how the txt2tags code works.
#
########################################################################
#
# The code that [1] parses the marked text is separated from the
# code that [2] insert the target tags.
#
#   [1] made by: def convert()
#   [2] made by: class BlockMaster
#
# The structures of the marked text are identified and its contents are
# extracted into a data holder (Python lists and dictionaries).
#
# When parsing the source file, the blocks (para, lists, quote, table)
# are opened with BlockMaster, right when found. Then its contents,
# which spans on several lines, are feeded into a special holder on the
# BlockMaster instance. Just when the block is closed, the target tags
# are inserted for the full block as a whole, in one pass. This way, we
# have a better control on blocks. Much better than the previous line by
# line approach.
#
# In other words, whenever inside a block, the parser *holds* the tag
# insertion process, waiting until the full block is read. That was
# needed primary to close paragraphs for the XHTML target, but
# proved to be a very good adding, improving many other processing.
#
# -------------------------------------------------------------------
#
# These important classes are all documented:
# CommandLine, SourceDocument, ConfigMaster, ConfigLines.
#
# There is a RAW Config format and all kind of configuration is first
# converted to this format. Then a generic method parses it.
#
# These functions get information about the input file(s) and take
# care of the init processing:
# get_infiles_config(), process_source_file() and convert_this_files()
#
########################################################################

#XXX Python coding warning
# Avoid common mistakes:
# - do NOT use newlist=list instead newlist=list[:]
# - do NOT use newdic=dic   instead newdic=dic.copy()
# - do NOT use dic[key]     instead dic.get(key)
# - do NOT use del dic[key] without key in dic before

#XXX Smart Image Align don't work if the image is a link
# Can't fix that because the image is expanded together with the
# link, at the linkbank filling moment. Only the image is passed
# to parse_images(), not the full line, so it is always 'middle'.

#XXX Paragraph separation not valid inside Quote
# Quote will not have <p></p> inside, instead will close and open
# again the <blockquote>. This really sux in CSS, when defining a
# different background color. Still don't know how to fix it.

#XXX TODO (maybe)
# New mark or macro which expands to an anchor full title.
# It is necessary to parse the full document in this order:
#  DONE  1st scan: HEAD: get all settings, including %!includeconf
#  DONE  2nd scan: BODY: expand includes & apply %!preproc
#        3rd scan: BODY: read titles and compose TOC info
#        4th scan: BODY: full parsing, expanding [#anchor] 1st
# Steps 2 and 3 can be made together, with no tag adding.
# Two complete body scans will be *slow*, don't know if it worths.
# One solution may be add the titles as postproc rules

# These are all the core Python modules used by txt2tags (KISS!)
import re
import os
import sys
import locale
import time  # %%date, %%mtime
import getopt
import textwrap
import csv
import struct
import unicodedata
import base64  # embedImage()
import shlex  # CommandLine.tokenize()

# import urllib  # read remote files (URLs) -- postponed, see issue 96
# import email  # %%mtime for remote files -- postponed, see issue 96

try:
    from lib import *
except:
    pass

import targets
from targets import _

# Program information
my_url = 'http://txt2tags.org'
my_name = 'txt2tags'
my_email = 'verde@aurelio.net'
my_revision = '$Revision: 1176 $'  # automatic, from SVN
my_version = '2.6'

# Add SVN revision number to version: 1.2.345
my_version = '%s.%s' % (my_version, re.sub(r'\D', '', my_revision))

# FLAGS   : the conversion related flags  , may be used in %!options
# OPTIONS : the conversion related options, may be used in %!options
# ACTIONS : the other behavior modifiers, valid on command line only
# MACROS  : the valid macros with their default values for formatting
# SETTINGS: global miscellaneous settings, valid on RC file only
# NO_TARGET: actions that don't require a target specification
# NO_MULTI_INPUT: actions that don't accept more than one input file
# CONFIG_KEYWORDS: the valid %!key:val keywords
#
# FLAGS and OPTIONS are configs that affect the converted document.
# They usually have also a --no-<option> to turn them OFF.
#
# ACTIONS are needed because when handling multiple input files, strange
# behavior may occur, such as use command line interface for the
# first file and gui for the second. There is no --no-<action>.
# Options --version and --help inside %!options are odd.
#
FLAGS = {
    'headers': 1,
    'enum-title': 0,
    'mask-email': 0,
    'toc-only': 0,
    'toc': 0,
    'qa': 0,
    'rc': 1,
    'css-sugar': targets.CONF['css-sugar'],
    'css-inside': 0,
    'quiet': 0,
    'fix-path': 0,
    'embed-images': 0,
    }
OPTIONS = {
    'target': '',
    'toc-level': 3,
    'toc-title': '',
    'style': '',
    'infile': '',
    'outfile': '',
    'encoding': '',
    'config-file': '',
    'split': 0,
    'lang': '',
    'width': 0,
    'height': 0,
    'chars': targets.CONF['chars'],
    'show-config-value': '',
    'template': '',
    'dirname': '',  # internal use only
    }
ACTIONS = {
    'help': 0,
    'version': 0,
    'gui': 0,
    'verbose': 0,
    'debug': 0,
    'dump-config': 0,
    'dump-source': 0,
    'targets': 0,
    }
MACROS = {
    # date
    'date': '%Y%m%d',
    'mtime': '%Y%m%d',
    # files
    'infile': '%f',
    'currentfile': '%f',
    'outfile': '%f',
    # app
    'appurl': '',
    'appname': '',
    'appversion': '',
    # conversion
    'target': '',
    'cmdline': '',
    'encoding': '',
    # header
    'header1': '',
    'header2': '',
    'header3': '',
    # Creative Commons license
    'cc': '',
    }
SETTINGS = {}  # for future use
NO_TARGET = [
    'help',
    'version',
    'gui',
    'toc-only',
    'dump-config',
    'dump-source',
    'targets',
    ]
NO_MULTI_INPUT = [
    'gui',
    'dump-config',
    'dump-source'
    ]
CONFIG_KEYWORDS = [
    'cc',
    'target',
    'encoding',
    'style',
    'stylepath',  # internal use only
    'options',
    'preproc',
    'postproc',
    'postvoodoo',
    'guicolors',
    ]


TARGET_NAMES = {
}


TARGET_TYPES = {
  'html'   : (_('HTML'), []),
  'wiki'   : (_('WIKI'), []),
  'office' : (_('OFFICE'), []),
  'text'   : (_('TEXT'), []),
}

TARGETS_LIST  = targets.TARGETS_LIST

OTHER_TARGETS = []
TARGET_ALIASES = {}
TARGET_EXTENSIONS = {}

for target in TARGETS_LIST:
    TARGET_NAMES[target] = getattr(getattr(targets, target), 'NAME', target.capitalize() + ' target')
    TARGET_EXTENSIONS[target] = getattr(getattr(targets, target), 'EXTENSION', target)
    for alias in getattr(getattr(targets, target), 'ALIASES', []):
        TARGET_ALIASES[alias] = target
    try:
        TARGET_TYPES[getattr(targets, target).TYPE][1].append(target)
    except:
        OTHER_TARGETS.append(target)

TARGETS = TARGET_NAMES.keys()
TARGETS.sort()

DEBUG = 0     # do not edit here, please use --debug
VERBOSE = 0   # do not edit here, please use -v, -vv or -vvv
QUIET = 0     # do not edit here, please use --quiet
GUI = 0       # do not edit here, please use --gui
AUTOTOC = 1   # do not edit here, please use --no-toc or %%toc

DFT_TEXT_WIDTH   = 72  # do not edit here, please use --width
DFT_SLIDE_WIDTH  = 80  # do not edit here, please use --width
DFT_SLIDE_HEIGHT = 25  # do not edit here, please use --height

# ASCII Art config
AA = targets.AA
AA_COUNT = 0
AA_PW_TOC = {}
AA_IMG = 0
AA_TITLE = ''
AA_MARKS = []

RC_RAW = []
CMDLINE_RAW = []
CONF = {}
BLOCK = None
TITLE = None
regex = {}
TAGS = {}
rules = {}
MAILING = ''

lang = 'english'
TARGET = ''

STDIN = STDOUT = '-'
MODULEIN = MODULEOUT = '-module-'
ESCCHAR   = '\x00'
SEPARATOR = '\x01'
LISTNAMES = {'-': 'list', '+': 'numlist', ':': 'deflist'}
LINEBREAK = {'default': '\n', 'win': '\r\n', 'mac': '\r'}

ESCAPES = {}

for target in TARGETS_LIST:
    ESCAPES[target] = getattr(getattr(targets, target), 'ESCAPES', [])

# Platform specific settings
LB = LINEBREAK.get(sys.platform[:3]) or LINEBREAK['default']

VERSIONSTR = _("%s version %s <%s>") % (my_name, my_version, my_url)


def Usage():
    fmt1 = "%4s  %-15s %s"
    fmt2 = "%4s, %-15s %s"
    return '\n'.join([
        '',
        _("Usage: %s [OPTIONS] [infile.t2t ...]") % my_name,
        '',
        fmt1 % (''  , '--targets'      , _("print a list of all the available targets and exit")),
        fmt2 % ('-t', '--target=TYPE'  , _("set target document type. currently supported:")),
        fmt1 % (''  , ''               , ', '.join(TARGETS[:8]) + ','),
        fmt1 % (''  , ''               , ', '.join(TARGETS[8:16]) + ','),
        fmt1 % (''  , ''               , ', '.join(TARGETS[16:25]) + ','),
        fmt1 % (''  , ''               , ', '.join(TARGETS[25:34]) + ','),
        fmt1 % (''  , ''               , ', '.join(TARGETS[34:])),
        fmt2 % ('-i', '--infile=FILE'  , _("set FILE as the input file name ('-' for STDIN)")),
        fmt2 % ('-o', '--outfile=FILE' , _("set FILE as the output file name ('-' for STDOUT)")),
        fmt1 % (''  , '--encoding=ENC' , _("inform source file encoding (UTF-8, iso-8859-1, etc)")),
        fmt1 % (''  , '--toc'          , _("add an automatic Table of Contents to the output")),
        fmt1 % (''  , '--toc-title=S'  , _("set custom TOC title to S")),
        fmt1 % (''  , '--toc-level=N'  , _("set maximum TOC level (depth) to N")),
        fmt1 % (''  , '--toc-only'     , _("print the Table of Contents and exit")),
        fmt2 % ('-n', '--enum-title'   , _("enumerate all titles as 1, 1.1, 1.1.1, etc")),
        fmt1 % (''  , '--style=FILE'   , _("use FILE as the document style (like HTML CSS)")),
        fmt1 % (''  , '--css-sugar'    , _("insert CSS-friendly tags for HTML/XHTML")),
        fmt1 % (''  , '--css-inside'   , _("insert CSS file contents inside HTML/XHTML headers")),
        fmt1 % (''  , '--embed-images' , _("embed image data inside HTML, html5, xhtml, RTF, aat and aap documents")),
        fmt2 % ('-H', '--no-headers'   , _("suppress header and footer from the output")),
        fmt2 % ('-T', '--template=FILE', _("use FILE as the template for the output document")),
        fmt1 % (''  , '--mask-email'   , _("hide email from spam robots. x@y.z turns <x (a) y z>")),
        fmt1 % (''  , '--width=N'      , _("set the output's width to N columns (used by aat, aap and aatw targets)")),
        fmt1 % (''  , '--height=N'     , _("set the output's height to N rows (used by aap target)")),
        fmt1 % (''  , '--chars=S'      , _("set the output's chars to S (used by all aa targets and rst)")),
        fmt1 % (''  , ''               , _("aa default " + targets.AA_SIMPLE + " rst default " + targets.RST_VALUES)),
        fmt2 % ('-C', '--config-file=F', _("read configuration from file F")),
        fmt1 % (''  , '--fix-path'     , _("fix resources path (image, links, CSS) when needed")),
        fmt1 % (''  , '--gui'          , _("invoke Graphical Tk Interface")),
        fmt2 % ('-q', '--quiet'        , _("quiet mode, suppress all output (except errors)")),
        fmt2 % ('-v', '--verbose'      , _("print informative messages during conversion")),
        fmt2 % ('-h', '--help'         , _("print this help information and exit")),
        fmt2 % ('-V', '--version'      , _("print program version and exit")),
        fmt1 % (''  , '--dump-config'  , _("print all the configuration found and exit")),
        fmt1 % (''  , '--dump-source'  , _("print the document source, with includes expanded")),
        '',
        _("Example:"),
        "     %s -t html --toc %s" % (my_name, _("file.t2t")),
        '',
        _("The 'no-' prefix disables the option:"),
        '     --no-toc, --no-style, --no-enum-title, ...',
        '',
        _("By default, converted output is saved to 'infile.<target>'."),
        _("Use --outfile to force an output file name."),
        _("If  input file is '-', reads from STDIN."),
        _("If output file is '-', dumps output to STDOUT."),
        '',
        my_url,
        '',
    ])


##############################################################################


# Here is all the target's templates
# You may edit them to fit your needs
#  - the %(HEADERn)s strings represent the Header lines
#  - the %(STYLE)s string is changed by --style contents
#  - the %(ENCODING)s string is changed by --encoding contents
#  - if any of the above is empty, the full line is removed
#  - use %% to represent a literal %
#
HEADER_TEMPLATE = {
}

for target in TARGETS_LIST:
    HEADER_TEMPLATE[target] = getattr(getattr(targets, target), 'HEADER', '')
    HEADER_TEMPLATE[target + 'css'] = getattr(getattr(targets, target), 'HEADERCSS', '')

##############################################################################


def getTags(config):
    "Returns all the known tags for the specified target"

    keys = """
    title1              numtitle1
    title2              numtitle2
    title3              numtitle3
    title4              numtitle4
    title5              numtitle5
    title1Open          title1Close
    title2Open          title2Close
    title3Open          title3Close
    title4Open          title4Close
    title5Open          title5Close
    blocktitle1Open     blocktitle1Close
    blocktitle2Open     blocktitle2Close
    blocktitle3Open     blocktitle3Close

    paragraphOpen       paragraphClose
    blockVerbOpen       blockVerbClose  blockVerbLine
    blockQuoteOpen      blockQuoteClose blockQuoteLine
    blockVerbSep
    blockCommentOpen    blockCommentClose

    fontMonoOpen        fontMonoClose
    fontBoldOpen        fontBoldClose
    fontItalicOpen      fontItalicClose
    fontUnderlineOpen   fontUnderlineClose
    fontStrikeOpen      fontStrikeClose

    listOpen            listClose
    listOpenCompact     listCloseCompact
    listItemOpen        listItemClose     listItemLine
    numlistOpen         numlistClose
    numlistOpenCompact  numlistCloseCompact
    numlistItemOpen     numlistItemClose  numlistItemLine
    deflistOpen         deflistClose
    deflistOpenCompact  deflistCloseCompact
    deflistItem1Open    deflistItem1Close
    deflistItem2Open    deflistItem2Close deflistItem2LinePrefix

    bar1                bar2
    url                 urlMark      urlMarkAnchor   urlImg
    email               emailMark
    img                 imgAlignLeft  imgAlignRight  imgAlignCenter
                       _imgAlignLeft _imgAlignRight _imgAlignCenter

    tableOpen           tableClose
    _tableBorder        _tableAlignLeft      _tableAlignCenter
    tableRowOpen        tableRowClose        tableRowSep
    tableTitleRowOpen   tableTitleRowClose
    tableCellOpen       tableCellClose       tableCellSep
    tableTitleCellOpen  tableTitleCellClose  tableTitleCellSep
    _tableColAlignLeft  _tableColAlignRight  _tableColAlignCenter
    tableCellAlignLeft  tableCellAlignRight  tableCellAlignCenter
    _tableCellAlignLeft _tableCellAlignRight _tableCellAlignCenter
    _tableCellColSpan   tableColAlignSep
    _tableCellColSpanChar                    _tableCellBorder
    _tableCellMulticolOpen
    _tableCellMulticolClose
    tableCellHead       tableTitleCellHead

    bodyOpen            bodyClose
    cssOpen             cssClose
    tocOpen             tocClose             TOC
    anchor
    comment
    pageBreak
    EOD
    """.split()

    # TIP: \a represents the current text inside the mark
    # TIP: ~A~, ~B~ and ~C~ are expanded to other tags parts

    alltags = {}
    for target in TARGETS_LIST:
        if getattr(getattr(targets, target), 'RULES', {}).get('confdependenttags'):
            reload(getattr(targets, target))
        alltags[target] = getattr(getattr(targets, target), 'TAGS', {})

    # Compose the target tags dictionary
    tags = {}
    target_tags = alltags[config['target']].copy()

    for key in keys:
        tags[key] = ''  # create empty keys
    for key in target_tags.keys():
        tags[key] = maskEscapeChar(target_tags[key])  # populate

    # Map strong line to pagebreak
    if rules['mapbar2pagebreak'] and tags['pageBreak']:
        tags['bar2'] = tags['pageBreak']

    # Change img tag if embedding images in RTF
    if config['embed-images']:
        if tags.get('imgEmbed'):
            tags['img'] = tags['imgEmbed']
        else:
            Error(_("Invalid --embed-images option with target '%s'." % config['target']))

    # Map strong line to separator if not defined
    if not tags['bar2'] and tags['bar1']:
        tags['bar2'] = tags['bar1']

    return tags


##############################################################################


def getRules(config):
    "Returns all the target-specific syntax rules"

    ret = {}
    allrules = [

        # target rules (ON/OFF)
        'linkable',               # target supports external links
        'tableable',              # target supports tables
        'tableonly',              # target computes only the tables
        'spread',                 # target uses the spread.py engine
        'spreadgrid',             # target adds the reference grid to the sheet
        'imglinkable',            # target supports images as links
        'imgalignable',           # target supports image alignment
        'imgasdefterm',           # target supports image as definition term
        'autonumberlist',         # target supports numbered lists natively
        'autonumbertitle',        # target supports numbered titles natively
        'stylable',               # target supports external style files
        'parainsidelist',         # lists items supports paragraph
        'compactlist',            # separate enclosing tags for compact lists
        'spacedlistitem',         # lists support blank lines between items
        'listnotnested',          # lists cannot be nested
        'listitemnotnested',      # list items must be closed before nesting lists
        'quotenotnested',         # quotes cannot be nested
        'verbblocknotescaped',    # don't escape specials in verb block
        'verbblockfinalescape',   # do final escapes in verb block
        'escapeurl',              # escape special in link URL
        'labelbeforelink',        # label comes before the link on the tag
        'onelinepara',            # dump paragraph as a single long line
        'onelinequote',           # dump quote as a single long line (EXPERIMENTAL)
        'notbreaklistitemclose',  # do not break line before the list item close tag (EXPERIMENTAL)
        'tabletitlerowinbold',    # manually bold any cell on table titles
        'tablecellstrip',         # strip extra spaces from each table cell
        'tablecellspannable',     # the table cells can have span attribute
        'tablecellmulticol',      # separate open+close tags for multicol cells
        'tablecolumnsnumber',     # set the number of columns in place of n_cols in tableOpen
        'tablenumber',            # set the number of the table in place of n_table in tableOpen
        'barinsidequote',         # bars are allowed inside quote blocks
        'finalescapetitle',       # perform final escapes on title lines
        'autotocnewpagebefore',   # break page before automatic TOC
        'autotocnewpageafter',    # break page after automatic TOC
        'autotocwithbars',        # automatic TOC surrounded by bars
        'plaintexttoc',           # TOC will be plain text (no links)
        'mapbar2pagebreak',       # map the strong bar to a page break
        'titleblocks',            # titles must be on open/close section blocks
        'listlineafteropen',      # put listItemLine after listItemOpen
        'escapexmlchars',         # escape the XML special chars: < > &
        'listlevelzerobased',     # list levels start at 0 when encoding into tags
        'zerodepthparagraph',     # non-nested paras have block depth of 0 instead of 1
        'cellspancumulative',     # cell span value adds up for each cell of a row
        'keepblankheaderline',    # template lines are not removed if headers are blank
        'confdependenttags',      # tags are configuration dependent
        'confdependentrules',     # rules are configuration dependent
        'asciiart',               # ascii art target
        'web',                    # html ascii art target
        'slides',                 # slides target

        # Target code beautify (ON/OFF)
        'indentverbblock',        # add leading spaces to verb block lines
        'breaktablecell',         # break lines after any table cell
        'breaktablelineopen',     # break line after opening table line
        'notbreaklistopen',       # don't break line after opening a new list
        'keepquoteindent',        # don't remove the leading TABs on quotes
        'keeplistindent',         # don't remove the leading spaces on lists
        'blankendautotoc',        # append a blank line at the auto TOC end
        'tagnotindentable',       # tags must be placed at the line beginning
        'spacedlistitemopen',     # append a space after the list item open tag
        'spacednumlistitemopen',  # append a space after the numlist item open tag
        'deflisttextstrip',       # strip the contents of the deflist text
        'blanksaroundpara',       # put a blank line before and after paragraphs
        'blanksaroundverb',       # put a blank line before and after verb blocks
        'blanksaroundquote',      # put a blank line before and after quotes
        'blanksaroundlist',       # put a blank line before and after lists
        'blanksaroundnumlist',    # put a blank line before and after numlists
        'blanksarounddeflist',    # put a blank line before and after deflists
        'blanksaroundnestedlist', # put a blank line before and after all type of nested lists
        'blanksaroundtable',      # put a blank line before and after tables
        'blanksaroundbar',        # put a blank line before and after bars
        'blanksaroundtitle',      # put a blank line before and after titles
        'blanksaroundnumtitle',   # put a blank line before and after numtitles
        'iswrapped',              # wrap with the --width value

        # Value settings
        'listmaxdepth',           # maximum depth for lists
        'quotemaxdepth',          # maximum depth for quotes
        'tablecellaligntype',     # type of table cell align: cell, column
        'blockdepthmultiply',     # block depth multiple for encoding
        'depthmultiplyplus',      # add to block depth before multiplying
        'cellspanmultiplier',     # cell span is multiplied by this value
        'spreadmarkup',           # the markup spread engine option: 'txt', 'html' or 'tex'
    ]

    rules_bank = {}

    targets.CONF['css-sugar'] = config['css-sugar']
    
    for target in TARGETS_LIST:
        if getattr(getattr(targets, target), 'RULES', {}).get('confdependentrules'):
            reload(getattr(targets, target))
        rules_bank[target] = getattr(getattr(targets, target), 'RULES', {})

    myrules = rules_bank[config['target']].copy()

    # Populate return dictionary
    for key in allrules:
        ret[key] = 0                         # reset all
    ret.update(myrules)                      # get rules

    return ret


##############################################################################

def getRegexes():
    "Returns all the regexes used to find the t2t marks"

    bank = {
    'blockVerbOpen':
        re.compile(r'^```\s*$'),
    'blockVerbClose':
        re.compile(r'^```\s*$'),
    'blockRawOpen':
        re.compile(r'^"""\s*$'),
    'blockRawClose':
        re.compile(r'^"""\s*$'),
    'blockTaggedOpen':
        re.compile(r"^'''\s*$"),
    'blockTaggedClose':
        re.compile(r"^'''\s*$"),
    'blockCommentOpen':
        re.compile(r'^%%%\s*$'),
    'blockCommentClose':
        re.compile(r'^%%%\s*$'),
    'quote':
        re.compile(r'^\t+'),
    '1lineVerb':
        re.compile(r'^``` (?=.)'),
    '1lineRaw':
        re.compile(r'^""" (?=.)'),
    '1lineTagged':
        re.compile(r"^''' (?=.)"),
    # mono, raw, bold, italic, underline:
    # - marks must be glued with the contents, no boundary spaces
    # - they are greedy, so in ****bold****, turns to <b>**bold**</b>
    'fontMono':
        re.compile(r'``([^\s](|.*?[^\s])`*)``'),
    'raw':
        re.compile(r'""([^\s](|.*?[^\s])"*)""'),
    'tagged':
        re.compile(r"''([^\s](|.*?[^\s])'*)''"),
    'fontBold':
        re.compile(r'\*\*([^\s](|.*?[^\s])\**)\*\*'),
    'fontItalic':
        re.compile(r'//([^\s](|.*?[^\s])/*)//'),
    'fontUnderline':
        re.compile(r'__([^\s](|.*?[^\s])_*)__'),
    'fontStrike':
        re.compile(r'--([^\s](|.*?[^\s])-*)--'),
    'list':
        re.compile(r'^( *)(-) (?=[^ ])'),
    'numlist':
        re.compile(r'^( *)(\+) (?=[^ ])'),
    'deflist':
        re.compile(r'^( *)(:) (.*)$'),
    'listclose':
        re.compile(r'^( *)([-+:])\s*$'),
    'bar':
        re.compile(r'^(\s*)([_=-]{20,})\s*$'),
    'table':
        re.compile(r'^ *\|(\||_|/)? '),
    'blankline':
        re.compile(r'^\s*$'),
    'comment':
        re.compile(r'^%'),

    # Auxiliary tag regexes
    '_imgAlign'          : re.compile(r'~A~', re.I),
    '_tableAlign'        : re.compile(r'~A~', re.I),
    '_anchor'            : re.compile(r'~A~', re.I),
    '_tableBorder'       : re.compile(r'~B~', re.I),
    '_tableColAlign'     : re.compile(r'~C~', re.I),
    '_tableCellColSpan'  : re.compile(r'~S~', re.I),
    '_tableCellAlign'    : re.compile(r'~A~', re.I),
    '_tableAttrDelimiter': re.compile(r'~Z~', re.I),
    '_blockDepth'        : re.compile(r'~D~', re.I),
    '_listLevel'         : re.compile(r'~L~', re.I),
    }

    # Special char to place data on TAGs contents  (\a == bell)
    bank['x'] = re.compile('\a')

    # %%macroname [ (formatting) ]
    bank['macros'] = re.compile(r'%%%%(?P<name>%s)\b(\((?P<fmt>.*?)\))?' % (
        '|'.join(MACROS.keys())), re.I)

    # %%TOC special macro for TOC positioning
    bank['toc'] = re.compile(r'^ *%%toc\s*$', re.I)

    # Almost complicated title regexes ;)
    titskel = r'^ *(?P<id>%s)(?P<txt>%s)\1(\[(?P<label>[\w-]*)\])?\s*$'
    bank['title']    = re.compile(titskel % ('[=]{1,5}', '[^=](|.*[^=])'))
    bank['numtitle'] = re.compile(titskel % ('[+]{1,5}', '[^+](|.*[^+])'))

    ### Complicated regexes begin here ;)
    #
    # Textual descriptions on --help's style: [...] is optional, | is OR

    ### First, some auxiliary variables
    #

    # [image.EXT]
    patt_img = r'\[([\w_,.+%$#@!?+~/-]+\.(png|jpe?g|gif|eps|bmp|svg))\]'

    # Link things
    # http://www.gbiv.com/protocols/uri/rfc/rfc3986.html
    # pchar: A-Za-z._~- / %FF / !$&'()*+,;= / :@
    # Recomended order: scheme://user:pass@domain/path?query=foo#anchor
    # Also works      : scheme://user:pass@domain/path#anchor?query=foo
    # TODO form: !'():
    urlskel = {
        'proto' : r'(https?|ftp|news|telnet|gopher|wais)://',
        'guess' : r'(www[23]?|ftp)\.',         # w/out proto, try to guess
        'login' : r'A-Za-z0-9_.-',             # for ftp://login@domain.com
        'pass'  : r'[^ @]*',                   # for ftp://login:pass@dom.com
        'chars' : r'A-Za-z0-9%._/~:,=$@&+-',   # %20(space), :80(port), D&D
        'anchor': r'A-Za-z0-9%._-',            # %nn(encoded)
        'form'  : r'A-Za-z0-9/%&=+:;.,$@*_-',  # .,@*_-(as is)
        'punct' : r'.,;:!?'
    }

    # username [ :password ] @
    patt_url_login = r'([%s]+(:%s)?@)?' % (urlskel['login'], urlskel['pass'])

    # [ http:// ] [ username:password@ ] domain.com [ / ]
    #     [ #anchor | ?form=data ]
    retxt_url = r'\b(%s%s|%s)[%s]+\b/*(\?[%s]+)?(#[%s]*)?' % (
        urlskel['proto'], patt_url_login, urlskel['guess'],
        urlskel['chars'], urlskel['form'], urlskel['anchor'])

    # filename | [ filename ] #anchor
    retxt_url_local = r'[%s]+|[%s]*(#[%s]*)' % (
        urlskel['chars'], urlskel['chars'], urlskel['anchor'])

    # user@domain [ ?form=data ]
    patt_email = r'\b[%s]+@([A-Za-z0-9_-]+\.)+[A-Za-z]{2,4}\b(\?[%s]+)?' % (
        urlskel['login'], urlskel['form'])

    # Saving for future use
    bank['_urlskel'] = urlskel

    ### And now the real regexes
    #

    bank['email'] = re.compile(patt_email, re.I)

    # email | url
    bank['link'] = re.compile(r'%s|%s' % (retxt_url, patt_email), re.I)

    # \[ label | imagetag    url | email | filename \]
    bank['linkmark'] = re.compile(
        r'\[(?P<label>%s|[^]]+) (?P<link>%s|%s|%s)\]' % (
            patt_img, retxt_url, patt_email, retxt_url_local),
        re.L + re.I)

    # Image
    bank['img'] = re.compile(patt_img, re.L + re.I)

    # Special things
    bank['special'] = re.compile(r'^%!\s*')
    return bank
### END OF regex nightmares


def completes_table(table):
    data = [[row['cells'], row['cellspan']] for row in table]
    n = max([len(line[0]) for line in data])
    data2 = []
    for line in data:
        if not line[1]:
            data2.append([n * [''], n * [1]])
        else:
            data2.append([line[0] + (n - sum(line[1])) * [''], line[1] + (n - sum(line[1])) * [1]])
    return data2


def convert_to_table(itera, headers, borders, center):
    if center:
        row_ini = ' | '
    else:
        row_ini = '| '
    if borders:
        row_end = ' |'
    else:
        row_end = ''
    table = []
    for row in itera:
        table.append(row_ini + ' | '.join(row).expandtabs() + row_end)
    if headers:
        table[0] = table[0].replace('|', '||', 1)
    return table


def parse_convert_table(table, tableable, target):
    ret = []
    # Note: cell contents is raw, no t2t marks are parsed
    if tableable:
        ret.extend(BLOCK.blockin('table'))
        if table:
            BLOCK.tableparser.__init__(table[0])
            for row in table:
                tablerow = TableMaster().parse_row(row)
                BLOCK.tableparser.add_row(tablerow)

                # Very ugly, but necessary for escapes
                line = SEPARATOR.join(tablerow['cells'])
                BLOCK.holdadd(doEscape(target, line))
            ret.extend(BLOCK.blockout())

    # Tables are mapped to verb when target is not table-aware
    else:
        ret.extend(BLOCK.blockin('verb'))
        BLOCK.propset('mapped', 'table')
        for row in table:
            BLOCK.holdadd(row)
        ret.extend(BLOCK.blockout())
    return ret


##############################################################################


class error(Exception):
    pass


def echo(msg):   # for quick debug
    print '\033[32;1m%s\033[m' % msg


def Quit(msg=''):
    if msg:
        print msg
    sys.exit(0)


def Error(msg):
    msg = _("%s: Error: ") % my_name + msg
    raise error(msg)


def getTraceback():
    try:
        from traceback import format_exception
        etype, value, tb = sys.exc_info()
        return ''.join(format_exception(etype, value, tb))
    except:
        pass


def getUnknownErrorMessage():
    msg = '%s\n%s (%s):\n\n%s' % (
        _('Sorry! Txt2tags aborted by an unknown error.'),
        _('Please send the following Error Traceback to the author'),
        my_email, getTraceback())
    return msg


def Message(msg, level):
    if level <= VERBOSE and not QUIET:
        prefix = '-' * 5
        print "%s %s" % (prefix * level, msg)


def Debug(msg, id_=0, linenr=None):
    "Show debug messages, categorized (colored or not)"
    if QUIET or not DEBUG:
        return
    if int(id_) not in range(8):
        id_ = 0
    # 0:black 1:red 2:green 3:yellow 4:blue 5:pink 6:cyan 7:white ;1:light
    ids            = ['INI', 'CFG', 'SRC', 'BLK', 'HLD', 'GUI', 'OUT', 'DET']
    colors_bgdark  = ['7;1', '1;1', '3;1', '6;1', '4;1', '5;1', '2;1', '7;1']
    colors_bglight = ['0'  , '1'  , '3'  , '6'  , '4'  , '5'  , '2'  , '0'  ]
    if linenr is not None:
        msg = "LINE %04d: %s" % (linenr, msg)
    if targets.COLOR_DEBUG:
        if targets.BG_LIGHT:
            color = colors_bglight[id_]
        else:
            color = colors_bgdark[id_]
        msg = '\033[3%sm%s\033[m' % (color, msg)
    print "++ %s: %s" % (ids[id_], msg)


def Readfile(file_path, remove_linebreaks=0, ignore_error=0):
    data = []

    # STDIN
    if file_path == '-':
        try:
            data = sys.stdin.readlines()
        except:
            if not ignore_error:
                Error(_('You must feed me with data on STDIN!'))

    # URL
    elif PathMaster().is_url(file_path):
        try:
            from urllib import urlopen
            f = urlopen(file_path)
            if f.getcode() == 404:  # URL not found
                raise
            data = f.readlines()
            f.close()
        except:
            if not ignore_error:
                Error(_("Cannot read file:") + ' ' + file_path)

    # local file
    else:
        try:
            f = open(file_path)
            data = f.readlines()
            f.close()
        except:
            if not ignore_error:
                Error(_("Cannot read file:") + ' ' + file_path)

    if remove_linebreaks:
        data = map(lambda x: re.sub('[\n\r]+$', '', x), data)

    Message(_("File read (%d lines): %s") % (len(data), file_path), 2)
    return data


def Savefile(file_path, contents):
    try:
        f = open(file_path, 'wb')
    except:
        Error(_("Cannot open file for writing:") + ' ' + file_path)
    if type(contents) == type([]):
        doit = f.writelines
    else:
        doit = f.write
    cont = []
    if CONF['encoding'].lower() == 'utf-8' and CONF['target'] != 'mgp':
        for line in contents:
            if isinstance(line, unicode):
                cont.append(line.encode('utf-8'))
            else:
                cont.append(line)
    elif CONF['target'] == 'mgp':
        for line in contents:
            if isinstance(line, unicode):
                cont.append(line.encode('latin1', 'replace'))
            else:
                cont.append(line)
    else:
        cont = contents
    doit(cont)
    f.close()


def showdic(dic):
    for k in dic.keys():
        print "%15s : %s" % (k, dic[k])


def dotted_spaces(txt=''):
    return txt.replace(' ', '.')


# TIP: win env vars http://www.winnetmag.com/Article/ArticleID/23873/23873.html
def get_rc_path():
    "Return the full path for the users' RC file"
    # Try to get the path from an env var. if yes, we're done
    user_defined = os.environ.get('T2TCONFIG')
    if user_defined:
        return user_defined
    # Env var not found, so perform automatic path composing
    # Set default filename according system platform
    rc_names = {'default': '.txt2tagsrc', 'win': '_t2trc'}
    rc_file = rc_names.get(sys.platform[:3]) or rc_names['default']
    # The file must be on the user directory, but where is this dir?
    rc_dir_search = ['HOME', 'HOMEPATH']
    for var in rc_dir_search:
        rc_dir = os.environ.get(var)
        if rc_dir:
            break
    # rc dir found, now we must join dir+file to compose the full path
    if rc_dir:
        # Compose path and return it if the file exists
        rc_path = os.path.join(rc_dir, rc_file)
        # On windows, prefix with the drive (%homedrive%: 2k/XP/NT)
        if sys.platform.startswith('win'):
            rc_drive = os.environ.get('HOMEDRIVE')
            rc_path = os.path.join(rc_drive, rc_path)
        return rc_path
    # Sorry, not found
    return ''


##############################################################################

class PathMaster:
    """Handle paths. See issues: 27, 62, 63, 71, 85."""

    def __init__(self):
        pass

    def is_url(self, text):
        return text.startswith('http://') or text.startswith('https://')

    def join(self, dirname, filename):
        """Join paths, unless filename is STDOUT, absolute or URL."""

        if not dirname \
            or not filename \
            or filename in (STDOUT, MODULEOUT) \
            or os.path.isabs(filename) \
            or self.is_url(filename):
            return filename
        else:
            return os.path.join(dirname, filename)

    def relpath(self, path, start):
        """Unlike os.path.relpath(), never touch URLs"""
        if not path or self.is_url(path):
            return path
        else:
            return os.path.relpath(path, start)


class CommandLine:
    """
    Command Line class - Masters command line

    This class checks and extract data from the provided command line.
    The --long options and flags are taken from the global OPTIONS,
    FLAGS and ACTIONS dictionaries. The short options are registered
    here, and also their equivalence to the long ones.

    _compose_short_opts() -> str
    _compose_long_opts() -> list
        Compose the valid short and long options list, on the
        'getopt' format.

    parse() -> (opts, args)
        Call getopt to check and parse the command line.
        It expects to receive the command line as a list, and
        without the program name (sys.argv[1:]).

    get_raw_config() -> [RAW config]
        Scans command line and convert the data to the RAW config
        format. See ConfigMaster class to the RAW format description.
        Optional 'ignore' and 'filter_' arguments are used to filter
        in or out specified keys.

    compose_cmdline(dict) -> [Command line]
        Compose a command line list from an already parsed config
        dictionary, generated from RAW by ConfigMaster(). Use
        this to compose an optimal command line for a group of
        options.

    The get_raw_config() calls parse(), so the typical use of this
    class is:

        raw = CommandLine().get_raw_config(sys.argv[1:])
    """
    def __init__(self):
        self.all_options = OPTIONS.keys()
        self.all_flags   = FLAGS.keys()
        self.all_actions = ACTIONS.keys()

        # short:long options equivalence
        self.short_long = {
            'C': 'config-file',
            'h': 'help',
            'H': 'no-headers',
            'i': 'infile',
            'n': 'enum-title',
            'o': 'outfile',
            'q': 'quiet',
            't': 'target',
            'T': 'template',
            'v': 'verbose',
            'V': 'version',
        }

        # Compose valid short and long options data for getopt
        self.short_opts = self._compose_short_opts()
        self.long_opts  = self._compose_long_opts()

    def _compose_short_opts(self):
        "Returns a string like 'hVt:o' with all short options/flags"
        ret = []
        for opt in self.short_long.keys():
            long_ = self.short_long[opt]
            if long_ in self.all_options:   # is flag or option?
                opt = opt + ':'             # option: have param
            ret.append(opt)
        #Debug('Valid SHORT options: %s' % ret)
        return ''.join(ret)

    def _compose_long_opts(self, extra=True):
        "Returns a list with all the valid long options/flags"
        ret = map(lambda x: x + '=', self.all_options)        # add =
        ret.extend(self.all_flags)                            # flag ON
        ret.extend(self.all_actions)                          # actions
        ret.extend(map(lambda x: 'no-' + x, self.all_flags))  # add no-*
        if extra:
            ret.extend(['no-style', 'no-encoding'])           # turn OFF
            ret.extend(['no-outfile', 'no-infile'])           # turn OFF
            ret.extend(['no-dump-config', 'no-dump-source'])  # turn OFF
            ret.extend(['no-targets'])                        # turn OFF
        #Debug('Valid LONG options: %s' % ret)
        return ret

    def tokenize(self, cmdline=''):
        "Convert a command line string to a list"
        return shlex.split(cmdline)

    def parse(self, cmdline=[]):
        "Check/Parse a command line list     TIP: no program name!"
        # Get the valid options
        short, long_ = self.short_opts, self.long_opts
        # Parse it!
        try:
            opts, args = getopt.getopt(cmdline, short, long_)
        except getopt.error, errmsg:
            Error(_("%s (try --help)") % errmsg)
        return (opts, args)

    def get_raw_config(self, cmdline=[], ignore=[], filter_=[], relative=0):
        "Returns the options/arguments found as RAW config"

        if not cmdline:
            return []
        ret = []

        # We need lists, not strings (such as from %!options)
        if type(cmdline) in (type(''), type(u'')):
            if isinstance(cmdline, unicode):
                cmdline = cmdline.encode('utf-8')
            cmdline = self.tokenize(cmdline)

        # Extract name/value pair of all configs, check for invalid names
        options, arguments = self.parse(cmdline[:])

        # Needed when expanding %!options inside remote %!includeconf
        dirname = ''

        # Some cleanup on the raw config
        for name, value in options:

            # Remove leading - and --
            name = re.sub('^--?', '', name)

            # Fix old misspelled --suGGar, --no-suGGar
            name = name.replace('suggar', 'sugar')

            # Translate short option to long
            if len(name) == 1:
                name = self.short_long[name]

            if name == 'dirname':
                dirname = value
                continue

            # Outfile exception: path relative to PWD
            if name == 'outfile' and value not in [STDOUT, MODULEOUT]:
                if relative:
                    value = os.path.abspath(value)
                else:
                    value = PathMaster().join(dirname, value)

            # -C, --config-file inclusion, path relative to PWD
            if name == 'config-file':
                value = PathMaster().join(dirname, value)
                ret.extend(ConfigLines().include_config_file(value))
                continue

            # --style: path relative to PWD
            # Already OK, when comming from the command line
            # Needs fix when coming from %!options: --style foo.css
            if name == 'style':
                ret.append(['all', 'stylepath', PathMaster().join(dirname, value)])

            # Save this config
            ret.append(['all', name, value])

        # All configuration was read and saved

        # Get infile, if any
        while arguments:
            infile = arguments.pop(0)
            ret.append(['all', 'infile', infile])

        # Apply 'ignore' and 'filter_' rules (filter_ is stronger)
        if (ignore or filter_):
            filtered = []
            for target, name, value in ret:
                if (filter_ and name in filter_) or \
                   (ignore and name not in ignore):
                    filtered.append([target, name, value])
                else:
                    fancykey = dotted_spaces("%12s" % name)
                    Message(_("Ignored config") + (" %s : %s" % (fancykey, value)), 3)
            ret = filtered[:]

        # Add the original command line string as 'realcmdline'
        ret.append(['all', 'realcmdline', cmdline])

        return ret

    def compose_cmdline(self, conf={}, no_check=0):
        "compose a full (and diet) command line from CONF dict"
        if not conf:
            return []
        args = []
        dft_options = OPTIONS.copy()
        cfg = conf.copy()
        valid_opts = self.all_options + self.all_flags
        use_short = {'no-headers': 'H', 'enum-title': 'n'}
        # Remove useless options
        if not no_check and cfg.get('toc-only'):
            if 'no-headers' in cfg:
                del cfg['no-headers']
            if 'outfile' in cfg:
                del cfg['outfile']      # defaults to STDOUT
            if cfg.get('target') == 'txt':
                del cfg['target']       # already default
            args.append('--toc-only')  # must be the first
            del cfg['toc-only']
        # Add target type
        if 'target' in cfg:
            args.append('-t ' + cfg['target'])
            del cfg['target']
        # Add other options
        for key in cfg.keys():
            if key not in valid_opts:
                continue  # may be a %!setting
            if key == 'outfile' or key == 'infile':
                continue  # later
            val = cfg[key]
            if not val:
                continue
            # Default values are useless on cmdline
            if val == dft_options.get(key):
                continue
            # -short format
            if key in use_short:
                args.append('-' + use_short[key])
                continue
            # --long format
            if key in self.all_flags:   # add --option
                args.append('--' + key)
            else:                       # add --option=value
                args.append('--%s=%s' % (key, val))
        # The outfile using -o
        if 'outfile' in cfg and \
           cfg['outfile'] != dft_options.get('outfile'):
            args.append('-o ' + cfg['outfile'])
        # Place input file(s) always at the end
        if 'infile' in cfg:
            args.append(' '.join(cfg['infile']))
        # Return as a nice list
        Debug("Diet command line: %s" % ' '.join(args), 1)
        return args


class BaseOptions(CommandLine):

    def __init__(self, cmdline=None, dft_options={}, dft_flags={}, short_long={}):

        # Available options
        self.dft_options = dft_options
        self.dft_flags = dft_flags
        self.short_long = short_long

        # Default values for all options
        self.defaults = {}
        self.defaults.update(self.dft_options)
        self.defaults.update(self.dft_flags)

        # Needed by self._compose_*_opts()
        self.all_flags = self.dft_flags.keys()
        self.all_options = self.dft_options.keys()
        self.all_actions = []

        # Compose valid short and long options data for getopt
        self.short_opts = self._compose_short_opts()
        self.long_opts = self._compose_long_opts(extra=False)

        # Got data? Parse it!
        if cmdline:
            self.raw = self.get_raw_config(cmdline)
            self.parsed = self.parse_raw()
        else:
            self.raw = []
            self.parsed = {}

    def get(self, key):
        return self.parsed.get(key, self.defaults[key])

    def parse(self, cmdline):
        try:
            opts, args = getopt.getopt(cmdline, self.short_opts, self.long_opts)
        except getopt.error, errmsg:
            Error(_("%s in %%!%s command") % (errmsg, self.__class__.__name__[:-7].upper()))
        return (opts, args)

    def parse_raw(self, raw=None):
        if not raw:
            raw = self.raw
        # Reset attributes to our modest needs
        cm = ConfigMaster(raw)
        cm.dft_options  = self.dft_options.copy()
        cm.dft_flags    = self.dft_flags.copy()
        cm.dft_actions  = {}
        cm.dft_settings = {}
        cm.incremental  = []
        cm.numeric      = []
        cm.multi        = []  # maybe in the future: ['infile']
        cm.defaults     = self.defaults.copy()
        cm.off          = cm._get_off()
        return cm.parse()


class CsvOptions(BaseOptions):
    """Tokenize and parse the %!CSV command arguments.
    
    When you find this line in the user document:
    
        %!CSV: -s tab foo.csv
    
    Just feed everything after the first : to this class,
    as a single string. It will be tokenized, parsed and
    saved to self.raw and self.parsed.
    
    Use the self.get() method to get the value of a config.
    If missing, the default value will be returned.
    
    Example:
        >>> import txt2tags, pprint
        >>> csvopt = txt2tags.CsvOptions('-s tab foo.csv')
        >>> pprint.pprint(csvopt.raw)
        [['all', 'separator', 'tab'],
         ['all', 'infile', 'foo.csv'],
         ['all', 'realcmdline', ['-s', 'tab', 'foo.csv']]]
        >>> pprint.pprint(csvopt.parsed)
        {'infile': 'foo.csv',
         'realcmdline': ['-s', 'tab', 'foo.csv'],
         'separator': 'tab'}
        >>> csvopt.get('separator')
        'tab'
        >>>
    """

    def __init__(self, cmdline=None):

        # Available options for %!CSV
        self.dft_options = {
            'separator': ',',
            'quotechar': '',
            'infile': '',
        }
        self.dft_flags = {
            'headers': 0,
            'borders': 0,
            'center': 0,
            'utf8': 0,
            'mailing': 0,
        }
        self.short_long = {
            'b': 'borders',
            'c': 'center',
            'h': 'headers',
            's': 'separator',
            'q': 'quotechar',
            'u': 'utf8',
            'm': 'mailing',
        }

        BaseOptions.__init__(self, cmdline, self.dft_options, self.dft_flags, self.short_long)


class DbOptions(BaseOptions):
    """Tokenize and parse the %!DB command arguments.
    
    When you find this line in the user document:
    
        %!DB: -q "select * from table" foo.db
    
    Just feed everything after the first : to this class,
    as a single string. It will be tokenized, parsed and
    saved to self.raw and self.parsed.
    
    Use the self.get() method to get the value of a config.
    If missing, the default value will be returned.
    
    Example:
        >>> import txt2tags, pprint
        >>> dbopt = txt2tags.DbOptions('-q "select * from table" foo.db')
        >>> pprint.pprint(dbopt.raw)
        [['all', 'query', 'select * from table'],
         ['all', 'infile', 'foo.db'],
         ['all', 'realcmdline', ['-q', 'select * from table', 'foo.db']]]
        >>> pprint.pprint(dbopt.parsed)
        {'infile': 'foo.db',
         'query': 'select * from table'}
        >>> dbopt.get('query')
        'select * from table'
        >>>
    """

    def __init__(self, cmdline=None):

        # Available options for %!DB
        self.dft_options = {
            'query': '',
            'infile': '',
        }
        self.dft_flags = {
            'borders': 0,
            'center': 0,
            'headers': 0,
            'mailing': 0,
        }
        self.short_long = {
            'b': 'borders',
            'c': 'center',
            'h': 'headers',
            'q': 'query',
            'm': 'mailing',
        }

        BaseOptions.__init__(self, cmdline, self.dft_options, self.dft_flags, self.short_long)


class FenOptions(BaseOptions):

    def __init__(self, cmdline=None):

        # Available options for %!FEN
        self.dft_options = {
            'infile': '',
        }
        self.dft_flags = {
            'unicode': 0,
        }
        self.short_long = {
            'u': 'unicode',
        }

        BaseOptions.__init__(self, cmdline, self.dft_options, self.dft_flags, self.short_long)


##############################################################################

class SourceDocument:
    """
    SourceDocument class - scan document structure, extract data

    It knows about full files. It reads a file and identify all
    the areas beginning (Head,Conf,Body). With this info it can
    extract each area contents.
    Note: the original line break is removed.

    DATA:
      self.arearef - Save Head, Conf, Body init line number
      self.areas   - Store the area names which are not empty
      self.buffer  - The full file contents (with NO \\r, \\n)

    METHODS:
      get()   - Access the contents of an Area. Example:
                config = SourceDocument(file).get('conf')

      split() - Get all the document Areas at once. Example:
                head, conf, body = SourceDocument(file).split()

    RULES:
        * The document parts are sequential: Head, Conf and Body.
        * One ends when the next begins.
        * The Conf Area is optional, so a document can have just
          Head and Body Areas.

        These are the Areas limits:
          - Head Area: the first three lines
          - Body Area: from the first valid text line to the end
          - Conf Area: the comments between Head and Body Areas

        Exception: If the first line is blank, this means no
        header info, so the Head Area is just the first line.
    """
    def __init__(self, filename='', contents=[]):
        self.areas = ['head', 'conf', 'body']
        self.arearef = []
        self.areas_fancy = ''
        self.filename = filename
        self.buffer = []
        if filename:
            self.scan_file(filename)
        elif contents:
            self.scan(contents)

    def split(self):
        "Returns all document parts, splitted into lists."
        return self.get('head'), self.get('conf'), self.get('body')

    def get(self, areaname):
        "Returns head|conf|body contents from self.buffer"
        # Sanity
        if areaname not in self.areas:
            return []
        if not self.buffer:
            return []
        # Go get it
        bufini = 1
        bufend = len(self.buffer)
        if   areaname == 'head':
            ini = bufini
            end = self.arearef[1] or self.arearef[2] or bufend
        elif areaname == 'conf':
            ini = self.arearef[1]
            end = self.arearef[2] or bufend
        elif areaname == 'body':
            ini = self.arearef[2]
            end = bufend
        else:
            Error("Unknown Area name '%s'" % areaname)
        lines = self.buffer[ini:end]
        # Make sure head will always have 3 lines
        while areaname == 'head' and len(lines) < 3:
            lines.append('')
        return lines

    def scan_file(self, filename):
        Debug("source file: %s" % filename)
        Message(_("Loading source document"), 1)
        buf = Readfile(filename, remove_linebreaks=1)
        self.scan(buf)

    def scan(self, lines):
        "Run through source file and identify head/conf/body areas"
        buf = lines
        if len(buf) == 0:
            Error(_('The input file is empty: %s') % self.filename)
        cfg_parser = ConfigLines().parse_line
        buf.insert(0, '')                         # text start at pos 1
        ref = [1, 4, 0]
        if not buf[1].strip():                    # no header
            ref[0] = 0
            ref[1] = 2
        rgx = getRegexes()
        on_comment_block = 0
        for i in xrange(ref[1], len(buf)):         # find body init:
            # Handle comment blocks inside config area
            if not on_comment_block \
               and rgx['blockCommentOpen'].search(buf[i]):
                on_comment_block = 1
                continue
            if on_comment_block \
               and rgx['blockCommentOpen'].search(buf[i]):
                on_comment_block = 0
                continue
            if on_comment_block:
                continue

            if buf[i].strip() and (            # ... not blank and
               buf[i][0] != '%' or             # ... not comment or
               rgx['macros'].match(buf[i]) or  # ... %%macro
               rgx['toc'].match(buf[i])    or  # ... %%toc
               cfg_parser(buf[i], 'include')[1] or  # ... %!include
               cfg_parser(buf[i], 'csv')[1] or # ... %!csv
               cfg_parser(buf[i], 'db')[1]  or # ... %!db
               cfg_parser(buf[i], 'fen')[1]    # ... %!fen
            ):
                ref[2] = i
                break
        if ref[1] == ref[2]:
            ref[1] = 0                          # no conf area
        for i in 0, 1, 2:                       # del !existent
            if ref[i] >= len(buf):
                ref[i] = 0                      # title-only
            if not ref[i]:
                self.areas[i] = ''
        Debug('Head,Conf,Body start line: %s' % ref)
        self.arearef = ref                      # save results
        self.buffer  = buf
        # Fancyness sample: head conf body (1 4 8)
        self.areas_fancy = "%s (%s)" % (
            ' '.join(self.areas),
            ' '.join(map(str, map(lambda x: x or '', ref))))
        Message(_("Areas found: %s") % self.areas_fancy, 2)

    def get_raw_config(self):
        "Handy method to get the CONF area RAW config (if any)"
        if not self.areas.count('conf'):
            return []
        Message(_("Scanning source document CONF area"), 1)
        raw = ConfigLines(
            file_=self.filename, lines=self.get('conf'),
            first_line=self.arearef[1]).get_raw_config()
        Debug("document raw config: %s" % raw, 1)
        return raw


##############################################################################

class ConfigMaster:
    """
    ConfigMaster class - the configuration wizard

    This class is the configuration master. It knows how to handle
    the RAW and PARSED config format. It also performs the sanity
    checking for a given configuration.

    DATA:
      self.raw         - Stores the config on the RAW format
      self.parsed      - Stores the config on the PARSED format
      self.defaults    - Stores the default values for all keys
      self.off         - Stores the OFF values for all keys
      self.multi       - List of keys which can have multiple values
      self.numeric     - List of keys which value must be a number
      self.incremental - List of keys which are incremental

    RAW FORMAT:
      The RAW format is a list of lists, being each mother list item
      a full configuration entry. Any entry is a 3 item list, on
      the following format: [ TARGET, KEY, VALUE ]
      Being a list, the order is preserved, so it's easy to use
      different kinds of configs, as CONF area and command line,
      respecting the precedence.
      The special target 'all' is used when no specific target was
      defined on the original config.

    PARSED FORMAT:
      The PARSED format is a dictionary, with all the 'key : value'
      found by reading the RAW config. The self.target contents
      matters, so this dictionary only contains the target's
      config. The configs of other targets are ignored.

    The CommandLine and ConfigLines classes have the get_raw_config()
    method to convert the configuration found to the RAW format.
    Just feed it to parse() and get a brand-new ready-to-use config
    dictionary. Example:

        >>> raw = CommandLine().get_raw_config(['-n', '-H'])
        >>> print raw
        [['all', 'enum-title', ''], ['all', 'no-headers', '']]
        >>> parsed = ConfigMaster(raw).parse()
        >>> print parsed
        {'enum-title': 1, 'headers': 0}
    """
    def __init__(self, raw=[], target=''):
        self.raw          = raw
        self.target       = target
        self.parsed       = {}
        self.dft_options  = OPTIONS.copy()
        self.dft_flags    = FLAGS.copy()
        self.dft_actions  = ACTIONS.copy()
        self.dft_settings = SETTINGS.copy()
        self.defaults     = self._get_defaults()
        self.off          = self._get_off()
        self.incremental  = ['verbose']
        self.numeric      = ['toc-level', 'split', 'width', 'height']
        self.multi        = ['infile', 'preproc', 'postproc', 'postvoodoo', 'options', 'style', 'stylepath']

    def _get_defaults(self):
        "Get the default values for all config/options/flags"
        empty = {}
        for kw in CONFIG_KEYWORDS:
            empty[kw] = ''
        empty.update(self.dft_options)
        empty.update(self.dft_flags)
        empty.update(self.dft_actions)
        empty.update(self.dft_settings)
        empty['realcmdline'] = ''  # internal use only
        empty['sourcefile']  = ''  # internal use only
        empty['currentsourcefile']  = ''  # internal use only
        return empty

    def _get_off(self):
        "Turns OFF all the config/options/flags"
        off = {}
        for key in self.defaults.keys():
            kind = type(self.defaults[key])
            if kind == type(9):
                off[key] = 0
            elif kind == type('') or kind == type(u''):
                off[key] = ''
            elif kind == type([]):
                off[key] = []
            else:
                Error('ConfigMaster: %s: Unknown type' % key)
        return off

    def _check_target(self):
        "Checks if the target is already defined. If not, do it"
        if not self.target:
            self.target = self.find_value('target')

    def get_target_raw(self):
        "Returns the raw config for self.target or 'all'"
        ret = []
        self._check_target()
        for entry in self.raw:
            if entry[0] == self.target or entry[0] == 'all':
                ret.append(entry)
        return ret

    def add(self, key, val):
        "Adds the key:value pair to the config dictionary (if needed)"
        # %!options
        if key == 'options':
            # Actions are not valid inside %!options
            ignoreme = self.dft_actions.keys()
            # --target inside %!options is not allowed (use %!target)
            ignoreme.append('target')
            # But there are some exceptions that are allowed (XXX why?)
            ignoreme.remove('dump-config')
            ignoreme.remove('dump-source')
            ignoreme.remove('targets')
            raw_opts = CommandLine().get_raw_config(
                val, ignore=ignoreme)
            for target, key, val in raw_opts:
                self.add(key, val)
            return
        # The no- prefix turns OFF this key
        if key.startswith('no-'):
            key = key[3:]              # remove prefix
            val = self.off.get(key)    # turn key OFF
        # Is this key valid?
        if key not in self.defaults:
            Debug('Bogus Config %s:%s' % (key, val), 1)
            return
        # Is this value the default one?
        if val == self.defaults.get(key):
            # If default value, remove previous key:val
            if key in self.parsed:
                del self.parsed[key]
            # Nothing more to do
            return
        # Flags ON comes empty. we'll add the 1 value now
        if val == '' and (
           key in self.dft_flags or
           key in self.dft_actions):
            val = 1
        # Multi value or single?
        if key in self.multi:
            # First one? start new list
            if key not in self.parsed:
                self.parsed[key] = []
            self.parsed[key].append(val)
        # Incremental value? so let's add it
        elif key in self.incremental:
            self.parsed[key] = (self.parsed.get(key) or 0) + val
        else:
            self.parsed[key] = val
        fancykey = dotted_spaces("%12s" % key)
        Message(_("Added config") + (" %s : %s" % (fancykey, val)), 3)

    def get_outfile_name(self, config={}):
        "Dirname is the same for {in,out}file"

        infile, outfile = config['sourcefile'], config['outfile']

        # Set output to STDOUT/MODULEOUT when no real inputfile
        if infile == STDIN and not outfile:
            outfile = STDOUT
        if infile == MODULEIN and not outfile:
            outfile = MODULEOUT

        # Automatic outfile name: infile.target
        if not outfile and (infile and TARGET_EXTENSIONS[config['target']]):
            # .t2t and .txt are the only "official" source extensions
            basename = re.sub('\.t[2x]t$', '', infile)
            outfile = "%s.%s" % (basename, TARGET_EXTENSIONS[config['target']])

        Debug(" infile: '%s'" % infile , 1)
        Debug("outfile: '%s'" % outfile, 1)
        return outfile

    def sanity(self, config, gui=0):
        "Basic config sanity checking"
        global AA
        if not config:
            return {}
        target = config.get('target')
        # Some actions don't require target specification
        if not target:
            for action in NO_TARGET:
                if config.get(action):
                    target = 'txt'
                    break
        if config.get('css-sugar'):
            targets.CONF['css-sugar'] = 1
        # On GUI, some checking are skipped
        if not gui:
            # We *need* a target
            if not target:
                Error(_('No target specified (try --help)') + '\n\n' +
                _('Please inform a target using the -t option or the %!target command.') + '\n' +
                _('Example:') + ' %s -t html %s' % (my_name, _('file.t2t')) + '\n\n' +
                _("Run 'txt2tags --targets' to see all the available targets."))
            # And of course, an infile also
            # TODO#1: It seems that this checking is never reached
            if not config.get('infile'):
                Error(_('Missing input file (try --help)'))
            # Is the target valid?
            if target not in TARGETS + TARGET_ALIASES.keys():
                Error(_("Invalid target '%s'") % target + '\n\n' +
                _("Run 'txt2tags --targets' to see all the available targets."))
            # alias becomes the target
            if target in TARGET_ALIASES.keys():
                TARGET_EXTENSIONS[TARGET_ALIASES[target]] = target
                target = TARGET_ALIASES[target]
        # Ensure all keys are present
        empty = self.defaults.copy()
        empty.update(config)
        config = empty.copy()
        # Check integers options
        for key in config.keys():
            if key in self.numeric:
                try:
                    config[key] = int(config[key])
                except ValueError:
                    Error(_('--%s value must be a number') % key)
        # Check split level value
        if config['split'] not in (0, 1, 2):
            Error(_('Option --split must be 0, 1 or 2'))
        # Check/set user ASCII Art formatting characters
        if config['chars']:
            targets.CONF['chars'] = config['chars']
            try:
                # Peace for ASCII 7-bits only
                config['chars'] = config['chars'].encode()
            except:
                if config['encoding'].lower() == 'utf-8' and locale.getpreferredencoding() != 'UTF-8':
                    Error(_("--chars: Expected chars from an UTF-8 terminal for your UTF-8 file"))
                if config['encoding'].lower() != 'utf-8' and locale.getpreferredencoding() == 'UTF-8':
                    if not config['encoding']:
                        Error(_("--chars: Expected an UTF-8 file for your chars from an UTF-8 terminal, you could set %!encoding: UTF-8"))
                    else:
                        Error(_("--chars: Expected an UTF-8 file for your chars from an UTF-8 terminal"))
            if target.startswith('aa'):
                if config['chars'] == 'unicode':
                    if config['encoding'].lower() != 'utf-8':
                        if not config['encoding']:
                            Error(_("--chars: Expected an UTF-8 file for the unicode chars set, you could set %!encoding: UTF-8"))
                        else:
                            Error(_("--chars: Expected an UTF-8 file for the unicode chars set"))
                    config['chars'] = unichr(0x250c) + unichr(0x2510) + unichr(0x2514) + unichr(0x2518) + unichr(0x252C) + unichr(0x2534) + unichr(0x251c) + unichr(0x2524) + unichr(0x255e) + unichr(0x256a) + unichr(0x2561) + unichr(0x256c) + unichr(0x2565) + unichr(0x256b) + unichr(0x2568) + unichr(0x253c) + unichr(0x2500) + unichr(0x2502) + unichr(0x2500) + unichr(0x2550) + unichr(0x2550) + unichr(0x2500) + '^"' + unichr(0x2043) + unichr(0x2550) + unichr(0x2551)
                if len(config['chars']) != len(targets.AA_SIMPLE) and len(config['chars']) != len(targets.AA_ADVANCED):
                    Error(_("--chars: Expected %i or %i chars, got %i") % (
                        len(targets.AA_SIMPLE), len(targets.AA_ADVANCED), len(config['chars'])))
                if isinstance(config['chars'], unicode):
                    for char in config['chars']:
                        if unicodedata.east_asian_width(char) in ('F', 'W'):
                            Error(_("--chars: Expected no CJK double width chars, but got %s") % char.encode('utf-8'))
                if len(config['chars']) == len(targets.AA_SIMPLE):
                    config['chars'] = 15 * config['chars'][0] + config['chars']
                targets.AA = dict(zip(targets.AA_KEYS, config['chars']))
                AA = targets.AA
            elif target == 'rst':
                if len(config['chars']) != len(targets.RST_VALUES):
                    Error(_("--chars: Expected %i chars, got %i") % (
                        len(targets.RST_VALUES), len(config['chars'])))
                else:
                    # http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#sections
                    chars_section = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
                    for char in config['chars'][:7]:
                        if char not in chars_section:
                            if locale.getpreferredencoding() == 'UTF-8':
                                char = char.encode('utf-8')
                            Error(_("--chars: Expected chars in : %s but got %s") % (chars_section, char))
                    # http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists
                    chars_bullet, char_8  = '*+-', config['chars'][7]
                    if char_8 not in chars_bullet:
                        if locale.getpreferredencoding() == 'UTF-8':
                            char_8 = char_8.encode('utf-8')
                        Error(_("--chars: Expected chars in : %s but got %s") % (chars_bullet, char_8))
                    targets.RST = dict(zip(targets.RST_KEYS, config['chars']))
            elif target in ('csv', 'csvs'):
                if len(config['chars']) != len(targets.CSV_VALUES) and len(config['chars']) != len(targets.CSV_VALUES) + 1:
                    Error(_("--chars: Expected %i or %i chars, got %i") % (
                        len(targets.CSV_VALUES), len(targets.CSV_VALUES) + 1, len(config['chars'])))
                else:
                    targets.CSV = dict(zip(targets.CSV_KEYS, config['chars']))

        # --toc-only is stronger than others
        if config['toc-only']:
            config['headers'] = 0
            config['toc']     = 0
            config['split']   = 0
            config['gui']     = 0
            config['outfile'] = config['outfile'] or STDOUT
        # Splitting is disable for now (future: HTML only, no STDOUT)
        config['split'] = 0
        # Restore target
        config['target'] = target
        # Set output file name
        config['outfile'] = self.get_outfile_name(config)
        # Checking suicide
        if os.path.abspath(config['sourcefile']) == os.path.abspath(config['outfile']) and \
           config['outfile'] not in [STDOUT, MODULEOUT] and not gui:
            Error(_("Input and Output files are the same: %s") % config['outfile'])
        if target == 'db':
            try:
                import sqlite3
            except:
                Error('No sqlite3 module')
            global DB, DBC
            try:
                os.remove(config['outfile'])
            except:
                pass
            DB = sqlite3.connect(config['outfile'])
            DBC = DB.cursor()
        return config

    def parse(self):
        "Returns the parsed config for the current target"
        raw = self.get_target_raw()
        for target, key, value in raw:
            if key == 'chars' and locale.getpreferredencoding() == 'UTF-8':
                self.add(key, value.decode('utf-8'))
            else:
                self.add(key, value)
        Message(_("Added the following keys: %s") % ', '.join(self.parsed.keys()), 2)
        return self.parsed.copy()

    def find_value(self, key='', target=''):
        "Scans ALL raw config to find the desired key"
        ret = []
        # Scan and save all values found
        for targ, k, val in self.raw:
            if k == key and (targ == target or targ == 'all'):
                ret.append(val)
        if not ret:
            return ''
        # If not multi value, return only the last found
        if key in self.multi:
            return ret
        else:
            return ret[-1]


########################################################################

class ConfigLines:
    """
    ConfigLines class - the config file data extractor

    This class reads and parse the config lines on the %!key:val
    format, converting it to RAW config. It deals with user
    config file (RC file), source document CONF area and
    %!includeconf directives.

    Call it passing a file name or feed the desired config lines.
    Then just call the get_raw_config() method and wait to
    receive the full config data on the RAW format. This method
    also follows the possible %!includeconf directives found on
    the config lines. Example:

        raw = ConfigLines(file=".txt2tagsrc").get_raw_config()

    The parse_line() method is also useful to be used alone,
    to identify and tokenize a single config line. For example,
    to get the %!include command components, on the source
    document BODY:

        target, key, value = ConfigLines().parse_line(body_line)
    """
    # parse_line regexes, moved here to avoid recompilation
    _parse_cfg = re.compile("""
        ^%!\s*                # leading id with opt spaces
        (?P<name>\w+)\s*       # config name
        (\((?P<target>\w*)\))? # optional target spec inside ()
        \s*:\s*               # key:value delimiter with opt spaces
        (?P<value>\S.+?)      # config value
        \s*$                  # rstrip() spaces and hit EOL
        """, re.I + re.VERBOSE)
    _parse_prepost = re.compile("""
                                      # ---[ PATTERN ]---
        ^( "([^"]*)"          # "double quoted" or
        | '([^']*)'           # 'single quoted' or
        | ([^\s]+)            # single_word
        )
        \s+                   # separated by spaces
                                  # ---[ REPLACE ]---
        ( "([^"]*)"           # "double quoted" or
        | '([^']*)'           # 'single quoted' or
        | (.*)                # anything
        )
        \s*$
        """, re.VERBOSE)
    _parse_guicolors = re.compile("^([^\s]+\s+){3}[^\s]+")  # 4 tokens

    def __init__(self, file_='', lines=[], first_line=1):
        self.file = file_ or 'NOFILE'
        self.lines = lines
        self.first_line = first_line
        if file_:
            self.folder = os.path.dirname(self.file)
        else:
            self.folder = ''

    def load_lines(self):
        "Make sure we've loaded the file contents into buffer"
        if not self.lines and not self.file:
            Error("ConfigLines: No file or lines provided")
        if not self.lines:
            self.lines = self.read_config_file(self.file)

    def read_config_file(self, filename=''):
        "Read a Config File contents, aborting on invalid line"
        if not filename:
            return []
        errormsg = _("Invalid CONFIG line on %s") + "\n%03d:%s"
        lines = Readfile(filename, remove_linebreaks=1)
        # Sanity: try to find invalid config lines
        for i in xrange(len(lines)):
            line = lines[i].rstrip()
            if not line:  # empty
                continue
            if line[0] != '%':
                Error(errormsg % (filename, i + 1, line))
        return lines

    def include_config_file(self, file_=''):
        "Perform the %!includeconf action, returning RAW config"
        if not file_:
            return []

        # Fix config file path
        file_ = self.fix_config_relative_path(file_)

        # Read and parse included config file contents
        return ConfigLines(file_=file_).get_raw_config()

    def fix_config_relative_path(self, path_):
        """
        The path for external files must be relative to the config file path.
        External files appear in: %!includeconf, %!style, %!template.
        See issue 71.
        """
        return PathMaster().join(self.folder, path_)

    def get_raw_config(self):
        "Scan buffer and extract all config as RAW (including includes)"
        ret = []
        self.load_lines()
        first = self.first_line

        def add(target, key, val):
            "Save the RAW config"
            ret.append([target, key, val])
            Message(_("Added %s") % key, 3)

        for i in xrange(len(self.lines)):
            line = self.lines[i]
            Message(_("Processing line %03d: %s") % (first + i, line), 2)
            target, key, val = self.parse_line(line)

            if not key:  # no config on this line
                continue

            # %!style
            # We need to fix the CSS files path. See issue 71.
            #
            # This stylepath config holds the fixed path for each CSS file.
            # This path is used when composing headers, inside doHeader().
            #
            if key == 'style':
                stylepath = self.fix_config_relative_path(val)
                add(target, 'stylepath', stylepath)
                # Note: the normal 'style' config will be added later

            # %!options
            if key == 'options':
                # Prepend --dirname option to track config file original folder
                if self.folder:
                    val = '--dirname %s %s' % (self.folder, val)

            # %!includeconf
            if key == 'includeconf':

                # Sanity
                err = _('A file cannot include itself (loop!)')
                if val == self.file:
                    Error("%s: %%!includeconf: %s" % (err, self.file))

                more_raw = self.include_config_file(val)
                ret.extend(more_raw)

                Message(_("Finished Config file inclusion: %s") % val, 2)

            # Normal config, except %!includeconf
            else:
                add(target, key, val)
        return ret

    def parse_line(self, line='', keyname='', target=''):
        "Detects %!key:val config lines and extract data from it"
        empty = ['', '', '']
        if not line:
            return empty
        no_target = ['target', 'includeconf']
        # XXX TODO <value>\S.+?  requires TWO chars, breaks %!include:a
        cfgregex  = ConfigLines._parse_cfg
        prepostregex = ConfigLines._parse_prepost
        guicolors = ConfigLines._parse_guicolors

        # Give me a match or get out
        match = cfgregex.match(line)
        if not match:
            return empty

        if keyname and keyname != match.group('name'):
            return empty
        if target and match.group('target') not in (None, '', target):
            return empty

        # Save information about this config
        name   = (match.group('name') or '').lower()
        target = (match.group('target') or 'all').lower()
        value  = match.group('value')

        # %!keyword(target) not allowed for these
        if name in no_target and match.group('target'):
            Error(
                _("You can't use (target) with %s") % ('%!' + name)
                + "\n%s" % line)

        # Force no_target keywords to be valid for all targets
        if name in no_target:
            target = 'all'

        # Special config for GUI colors
        if name == 'guicolors':
            valmatch = guicolors.search(value)
            if not valmatch:
                return empty
            value = re.split('\s+', value)

        # Special config with two quoted values (%!preproc: "foo" 'bar')
        if name in ['preproc', 'postproc', 'postvoodoo']:
            valmatch = prepostregex.search(value)
            if not valmatch:
                return empty
            getval = valmatch.group
            patt   = getval(2) or getval(3) or getval(4) or ''
            repl   = getval(6) or getval(7) or getval(8) or ''
            value  = (patt, repl)
        return [target, name, value]


##############################################################################

class MaskMaster:
    """(Un)Protect important structures from escaping and formatting.

    Some inline markup must be protected, because its contents may match
    other markup, or because we should not escape or format its contents
    in any way.

    When the source line is read, we call the mask() method to identify
    those inliners (link, mono, macro, raw, tagged) and change each one
    for an (ugly) internal identifier.

    For example, ''<b>this</b>'' will become vvvTAGGED0vvv. The number
    increases as other inliners of the same type are found. 

    The method undo() is called at the end of the line processing,
    expanding all masks back to their original (untouched) content.
    """

    def __init__(self):
        self.linkmask   = 'vvvLINKNNNvvv'  # NNN will be replaced by the index
        self.monomask   = 'vvvMONONNNvvv'
        self.macromask  = 'vvvMACRONNNvvv'
        self.rawmask    = 'vvvRAWNNNvvv'
        self.taggedmask = 'vvvTAGGEDNNNvvv'
        self.tocmask    = 'vvvTOCvvv'
        self.linkmaskre   = re.compile('vvvLINK(\d+)vvv')
        self.monomaskre   = re.compile('vvvMONO(\d+)vvv')
        self.macromaskre  = re.compile('vvvMACRO(\d+)vvv')
        self.rawmaskre    = re.compile('vvvRAW(\d+)vvv')
        self.taggedmaskre = re.compile('vvvTAGGED(\d+)vvv')
        self.macroman   = MacroMaster()
        self.reset()

    def reset(self):
        self.linkbank = []
        self.monobank = []
        self.macrobank = []
        self.rawbank = []
        self.taggedbank = []

    def mask(self, line=''):
        global AUTOTOC

        # The verbatim, raw and tagged inline marks are mutually exclusive.
        # This means that one can't appear inside the other.
        # If found, the inner marks must be ignored.
        # Example: ``foo ""bar"" ''baz''``
        # In HTML: <code>foo ""bar"" ''baz''</code>
        #
        # The trick here is to protect the mark who appears first on the line.
        # The three regexes are tried and the one with the lowest index wins.
        # If none is found (else), we get out of the loop.
        #
        while True:

            # Try to match the line for the three marks
            # Note: 'z' > 99999999...
            #
            t = r = v = 'z'
            try:
                t = regex['tagged'].search(line).start()
            except:
                pass
            try:
                r = regex['raw'].search(line).start()
            except:
                pass
            try:
                v = regex['fontMono'].search(line).start()
            except:
                pass

            # Protect tagged text
            if t >= 0 and t < r and t < v:
                txt = regex['tagged'].search(line).group(1)
                txt = doProtect(TARGET, txt)
                i = len(self.taggedbank)
                self.taggedbank.append(txt)
                mask = self.taggedmask.replace('NNN', str(i))
                line = regex['tagged'].sub(mask, line, 1)

            # Protect raw text
            elif r >= 0 and r < t and r < v:
                txt = regex['raw'].search(line).group(1)
                txt = doEscape(TARGET, txt)
                i = len(self.rawbank)
                self.rawbank.append(txt)
                mask = self.rawmask.replace('NNN', str(i))
                line = regex['raw'].sub(mask, line, 1)

            # Protect verbatim text
            elif v >= 0 and v < t and v < r:
                txt = regex['fontMono'].search(line).group(1)
                txt = doEscape(TARGET, txt)
                i = len(self.monobank)
                self.monobank.append(txt)
                mask = self.monomask.replace('NNN', str(i))
                line = regex['fontMono'].sub(mask, line, 1)
            else:
                break

        # Protect macros
        while regex['macros'].search(line):
            txt = regex['macros'].search(line).group()
            i = len(self.macrobank)
            self.macrobank.append(txt)
            mask = self.macromask.replace('NNN', str(i))
            line = regex['macros'].sub(mask, line, 1)

        # Protect TOC location
        while regex['toc'].search(line):
            line = regex['toc'].sub(self.tocmask, line)
            AUTOTOC = 0

        # Protect URLs and emails
        while regex['linkmark'].search(line) or \
              regex['link'].search(line):

            # Try to match plain or named links
            match_link  = regex['link'].search(line)
            match_named = regex['linkmark'].search(line)

            # Define the current match
            if match_link and match_named:
                # Both types found, which is the first?
                m = match_link
                if match_named.start() < match_link.start():
                    m = match_named
            else:
                # Just one type found, we're fine
                m = match_link or match_named

            # Extract link data and apply mask
            if m == match_link:              # plain link
                link = m.group()
                label = ''
                link_re = regex['link']
            else:                            # named link
                link = fix_relative_path(m.group('link'))
                label = m.group('label').rstrip()
                link_re = regex['linkmark']

            # Save link data to the link bank
            i = len(self.linkbank)
            self.linkbank.append((label, link))

            # Mask the link mark in the original line
            mask = self.linkmask.replace('NNN', str(i))
            line = link_re.sub(mask, line, 1)

        return line

    def undo(self, line):

        # url & email
        matches = list(self.linkmaskre.finditer(line))
        while matches:
            m = matches.pop()
            i = int(m.group(1))
            label, url = self.linkbank[i]
            link = get_tagged_link(label, url)
            line = line[0:m.start()] + link + line[m.end():]

        # Expand macros
        matches = list(self.macromaskre.finditer(line))
        while matches:
            m = matches.pop()
            i = int(m.group(1))
            macro = self.macroman.expand(self.macrobank[i])
            line = line[0:m.start()] + macro + line[m.end():]

        # Expand verb
        matches = list(self.monomaskre.finditer(line))
        while matches:
            m = matches.pop()
            i = int(m.group(1))
            open_, close = TAGS['fontMonoOpen'], TAGS['fontMonoClose']
            line = line[0:m.start()] + open_ + self.monobank[i] + close + line[m.end():]

        # Expand raw
        matches = list(self.rawmaskre.finditer(line))
        while matches:
            m = matches.pop()
            i = int(m.group(1))
            line = line[0:m.start()] + self.rawbank[i] + line[m.end():]

        # Expand tagged
        matches = list(self.taggedmaskre.finditer(line))
        while matches:
            m = matches.pop()
            i = int(m.group(1))
            line = line[0:m.start()] + self.taggedbank[i] + line[m.end():]

        return line


##############################################################################


class TitleMaster:
    "Title things"
    def __init__(self):
        self.count = ['', 0, 0, 0, 0, 0]
        self.toc   = []
        self.level = 0
        self.kind  = ''
        self.txt   = ''
        self.label = ''
        self.tag   = ''
        self.tag_hold = []
        self.last_level = 0
        self.count_id = ''
        self.anchor_count = 0
        self.anchor_prefix = 'toc'

    def _open_close_blocks(self):
        "Open new title blocks, closing the previous (if any)"
        if not rules['titleblocks']:
            return
        tag = ''
        last = self.last_level
        curr = self.level

        # Same level, just close the previous
        if curr == last:
            tag = TAGS.get('title%dClose' % last)
            if tag:
                self.tag_hold.append(tag)

        # Section -> subsection, more depth
        while curr > last:
            last += 1

            # Open the new block of subsections
            tag = TAGS.get('blockTitle%dOpen' % last)
            if tag:
                self.tag_hold.append(tag)

            # Jump from title1 to title3 or more
            # Fill the gap with an empty section
            if curr - last > 0:
                tag = TAGS.get('title%dOpen' % last)
                tag = regex['x'].sub('', tag)      # del \a
                if tag:
                    self.tag_hold.append(tag)

        # Section <- subsection, less depth
        while curr < last:
            # Close the current opened subsection
            tag = TAGS.get('title%dClose' % last)
            if tag:
                self.tag_hold.append(tag)

            # Close the current opened block of subsections
            tag = TAGS.get('blockTitle%dClose' % last)
            if tag:
                self.tag_hold.append(tag)

            last -= 1

            # Close the previous section of the same level
            # The subsections were under it
            if curr == last:
                tag = TAGS.get('title%dClose' % last)
                if tag:
                    self.tag_hold.append(tag)

    def add(self, line):
        "Parses a new title line."
        if not line:
            return
        self._set_prop(line)
        self._open_close_blocks()
        self._set_count_id()
        self._set_label()
        self._save_toc_info()

    def close_all(self):
        "Closes all opened title blocks"
        ret = []
        ret.extend(self.tag_hold)
        while self.level:
            tag = TAGS.get('title%dClose' % self.level)
            if tag:
                ret.append(tag)
            tag = TAGS.get('blockTitle%dClose' % self.level)
            if tag:
                ret.append(tag)
            self.level -= 1
        return ret

    def _save_toc_info(self):
        "Save TOC info, used by self.dump_marked_toc()"
        self.toc.append((self.level, self.count_id, self.txt, self.label))

    def _set_prop(self, line=''):
        "Extract info from original line and set data holders."
        # Detect title type (numbered or not)
        id_ = line.lstrip()[0]
        if   id_ == '=':
            kind = 'title'
        elif id_ == '+':
            kind = 'numtitle'
        else:
            Error("Unknown Title ID '%s'" % id_)
        # Extract line info
        match = regex[kind].search(line)
        level = len(match.group('id'))
        txt   = match.group('txt').strip()
        label = match.group('label')
        # Parse info & save
        if CONF['enum-title']:
            kind = 'numtitle'  # force
        if rules['titleblocks']:
            self.tag = TAGS.get('%s%dOpen' % (kind, level)) or \
                       TAGS.get('title%dOpen' % level)
        else:
            self.tag = TAGS.get(kind + str(level)) or \
                       TAGS.get('title' + str(level))
        self.last_level = self.level
        self.kind  = kind
        self.level = level
        self.txt   = txt
        self.label = label

    def _set_count_id(self):
        "Compose and save the title count identifier (if needed)."
        count_id = ''
        if self.kind == 'numtitle' and not rules['autonumbertitle']:
            # Manually increase title count
            self.count[self.level] += 1
            # Reset sublevels count (if any)
            max_levels = len(self.count)
            if self.level < max_levels - 1:
                for i in xrange(self.level + 1, max_levels):
                    self.count[i] = 0
            # Compose count id from hierarchy
            for i in xrange(self.level):
                count_id = "%s%d." % (count_id, self.count[i + 1])
        self.count_id = count_id

    def _set_label(self):
        "Compose and save title label, used by anchors."
        # Remove invalid chars from label set by user
        self.label = re.sub('[^A-Za-z0-9_-]', '', self.label or '')
        # Generate name as 15 first :alnum: chars
        #TODO how to translate safely accented chars to plain?
        #self.label = re.sub('[^A-Za-z0-9]', '', self.txt)[:15]
        # 'tocN' label - sequential count, ignoring 'toc-level'
        #self.label = self.anchor_prefix + str(len(self.toc) + 1)

    def _get_tagged_anchor(self):
        "Return anchor if user defined a label, or TOC is on."
        ret = ''
        label = self.label
        if CONF['toc'] and self.level <= CONF['toc-level']:
            # This count is needed bcos self.toc stores all
            # titles, regardless of the 'toc-level' setting,
            # so we can't use self.toc length to number anchors
            self.anchor_count += 1
            # Autonumber label (if needed)
            label = label or '%s%s' % (self.anchor_prefix, self.anchor_count)
        if label and TAGS['anchor']:
            ret = regex['x'].sub(label, TAGS['anchor'])
        return ret

    def _get_full_title_text(self):
        "Returns the full title contents, already escaped."
        ret = self.txt
        # Insert count_id (if any) before text
        if self.count_id:
            ret = '%s %s' % (self.count_id, ret)
        # Escape specials
        ret = doEscape(TARGET, ret)
        # Same targets needs final escapes on title lines
        # It's here because there is a 'continue' after title
        if rules['finalescapetitle']:
            ret = doFinalEscape(TARGET, ret)
        return ret

    def get(self):
        "Returns the tagged title as a list."
        global AA_TITLE
        ret = []

        # Maybe some anchoring before?
        anchor = self._get_tagged_anchor()
        self.tag = regex['_anchor'].sub(anchor, self.tag)

        ### Compose & escape title text (TOC uses unescaped)
        full_title = self._get_full_title_text()

        # Close previous section area
        ret.extend(self.tag_hold)
        self.tag_hold = []

        tagged = regex['x'].sub(full_title, self.tag)

        # Adds "underline" on TXT target
        if TARGET == 'txt':
            if BLOCK.count > 1:
                ret.append('')  # blank line before
            ret.append(tagged)
            i = aa.lencjk(full_title)
            ret.append(regex['x'].sub('=' * i, self.tag))
        elif rules['asciiart'] and self.level == 1:
            if rules['slides'] :
                AA_TITLE = tagged
            else :
                if BLOCK.count > 1:
                    ret.append('')  # blank line before
                box = aa.box([tagged], AA, CONF['width'])
                if rules['web'] and CONF['toc']:
                    ret.extend([anchor] + box + ['</a>'])
                else:
                    ret.extend(box)
        elif rules['asciiart']:
            level = 'level' + str(self.level)
            if BLOCK.count > 1:
                ret.append('')  # blank line before
            if rules['slides']:
                under = aa.under(tagged, AA[level], CONF['width'] - 2, False)
            else:
                under = aa.under(tagged, AA[level], CONF['width'], False)
            if rules['web'] and CONF['toc'] and self.level <= CONF['toc-level'] and not rules['slides']:
                ret.extend([anchor] + under + ['</a>'])
            else:
                ret.extend(under)
        elif TARGET == 'rst' and self.level == 1:
            if BLOCK.count > 1:
                ret.append('')  # blank line before
            ret.extend(aa.under(tagged, targets.RST['level1'], 10000, True))
        elif TARGET == 'rst':
            level = 'level' + str(self.level)
            if BLOCK.count > 1:
                ret.append('')  # blank line before
            ret.extend(aa.under(tagged, targets.RST[level], 10000, False))
        else:
            ret.append(tagged)
        return ret

    def dump_marked_toc(self, max_level=99):
        "Dumps all toc itens as a valid t2t-marked list"
        ret = []
        toc_count = 1
        head = 0
        if CONF['headers'] and CONF['header1']:
            head = 1
        for level, count_id, txt, label in self.toc:
            if level > max_level:  # ignore
                continue
            indent = '  ' * level
            id_txt = ('%s %s' % (count_id, txt)).lstrip()
            if rules['asciiart'] and rules['slides']:
                indent = '  ' * (level - 1)
                if rules['web'] and not CONF['toc-only']:
                    label = str(AA_PW_TOC[txt] / (CONF['height'] - 1) + head + 2) + '.0'
            label = label or self.anchor_prefix + str(toc_count)
            toc_count += 1

            # TOC will have crosslinks to anchors
            if TAGS['anchor']:
                if CONF['enum-title'] and level == 1:
                    # 1. [Foo #anchor] is more readable than [1. Foo #anchor] in level 1.
                    # This is a stoled idea from Windows .CHM help files.
                    tocitem = '%s+ [""%s"" #%s]' % (indent, txt, label)
                else:
                    tocitem = '%s- [""%s"" #%s]' % (indent, id_txt, label)

            # TOC will be plain text (no links)
            else:
                if rules['plaintexttoc'] and not rules['slides']:
                    # For these, the list is not necessary, just dump the text
                    tocitem = '%s""%s""' % (indent, id_txt)
                elif rules['asciiart'] and CONF['enum-title'] and level == 1:
                    tocitem = '%s+ ""%s""' % (indent, txt)
                else:
                    tocitem = '%s- ""%s""' % (indent, id_txt)
            ret.append(tocitem)
        return ret


##############################################################################

# Table syntax reference for targets:
# http://www.mediawiki.org/wiki/Help:Tables
# http://moinmo.in/HelpOnMoinWikiSyntax#Tables
# http://moinmo.in/HelpOnTables
# http://www.wikicreole.org/wiki/Creole1.0#section-Creole1.0-Tables
# http://www.wikicreole.org/wiki/Tables
# http://www.pmwiki.org/wiki/PmWiki/Tables
# http://www.dokuwiki.org/syntax#tables
# http://michelf.com/projects/php-markdown/extra/#table
# http://code.google.com/p/support/wiki/WikiSyntax#Tables
# http://www.biblioscape.com/rtf15_spec.htm
# http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#tables
#
# Good reading:
# http://www.wikicreole.org/wiki/ListOfTableMarkups
#
# See also:
# test/marks/table.t2t
# test/target/table.t2t

#TODO check all this table mess
# It uses parse_row properties for table lines
# BLOCK.table() replaces the cells by the parsed content
#
class TableMaster:
    def __init__(self, line=''):
        self.rows      = []
        self.border    = 0
        self.align     = 'Left'
        self.cellalign = []
        self.colalign  = []
        self.cellspan  = []
        if line:
            prop = self.parse_row(line)
            self.border    = prop['border']
            self.title     = prop['title']
            self.vert_head = prop['vert_head']
            self.align     = prop['align']
            self.cellalign = prop['cellalign']
            self.cellspan  = prop['cellspan']
            self.n_cols    = sum(self.cellspan)
            self.colalign  = self._get_col_align()

    def _get_col_align(self):
        colalign = []
        for cell in range(0, len(self.cellalign)):
            align = self.cellalign[cell]
            span  = self.cellspan[cell]
            colalign.extend([align] * span)
        return colalign

    def _get_full_tag(self, topen):
        # topen     = TAGS['tableOpen']
        tborder   = TAGS['_tableBorder']
        talign    = TAGS['_tableAlign' + self.align]
        calignsep = TAGS['tableColAlignSep']
        calign    = ''

        # The first line defines if table has border or not
        if not self.border:
            tborder = ''
        # Set the columns alignment
        if rules['tablecellaligntype'] == 'column':
            calign = map(lambda x: TAGS['_tableColAlign%s' % x], self.colalign)
            calign = calignsep.join(calign)
        # Align full table, set border and Column align (if any)
        topen = regex['_tableAlign'].sub(talign , topen)
        topen = regex['_tableBorder'].sub(tborder, topen)
        topen = regex['_tableColAlign'].sub(calign , topen)
        # Tex table spec, border or not: {|l|c|r|} , {lcr}
        if calignsep and not self.border:
            # Remove cell align separator
            topen = topen.replace(calignsep, '')
        return topen

    def _get_cell_align(self, cells):
        ret = []
        for cell in cells:
            align = 'Left'
            if cell.strip():
                if cell[0] == ' ' and cell[-1] == ' ':
                    align = 'Center'
                elif cell[0] == ' ':
                    align = 'Right'
            ret.append(align)
        return ret

    def _get_cell_span(self, cells):
        ret = []
        for cell in cells:
            span = 1
            m = re.search('\a(\|+)$', cell)
            if m:
                span = len(m.group(1)) + 1
            ret.append(span)
        return ret

    def _tag_cells(self, rowdata):
        cells = rowdata['cells']
        open_ = TAGS['tableCellOpen']
        close = TAGS['tableCellClose']
        sep = TAGS['tableCellSep']
        head = TAGS['tableCellHead']
        calign = map(lambda x: TAGS['_tableCellAlign' + x], rowdata['cellalign'])
        caligntag = map(lambda x: TAGS['tableCellAlign' + x], rowdata['cellalign'])
        calignsep = TAGS['tableColAlignSep']
        ncolumns = len(self.colalign)

        # Populate the span and multicol open tags
        cspan = []
        multicol = []
        colindex = 0

        thisspan = 0
        spanmultiplier = rules['cellspanmultiplier'] or 1

        cellhead = []
        cellbody = []

        for cellindex in range(0, len(rowdata['cellspan'])):

            span = rowdata['cellspan'][cellindex]
            align = rowdata['cellalign'][cellindex]

            # hack to get cell size/span into rtf, in twips
            if rules['cellspancumulative']:
                thisspan += span
            else:
                thisspan = span
            span = thisspan * spanmultiplier

            if span > 1:

                if TAGS['_tableCellColSpanChar']:
                    # spanchar * n
                    cspan.append(TAGS['_tableCellColSpanChar'] * (span - 1))
                    # Note: using -1 for moin, where spanchar == cell delimiter
                else:
                    # \a replaced by n
                    cspan.append(regex['x'].sub(str(span), TAGS['_tableCellColSpan']))

                mcopen = regex['x'].sub(str(span), TAGS['_tableCellMulticolOpen'])
                multicol.append(mcopen)
            else:
                cspan.append('')

                if colindex < ncolumns and align != self.colalign[colindex]:
                    mcopen = regex['x'].sub('1', TAGS['_tableCellMulticolOpen'])
                    multicol.append(mcopen)
                else:
                    multicol.append('')

            if not self.border:
                multicol[-1] = multicol[-1].replace(calignsep, '')

            colindex += span

        # Maybe is it a title row?
        if rowdata['title']:
            # Defaults to normal cell tag if not found
            open_ = TAGS['tableTitleCellOpen']  or open_
            close = TAGS['tableTitleCellClose'] or close
            sep   = TAGS['tableTitleCellSep']   or sep
            head  = TAGS['tableTitleCellHead']  or head

        # Should we break the line on *each* table cell?
        if rules['breaktablecell']:
            close = close + '\n'

        # Cells pre processing
        if rules['tablecellstrip']:
            cells = map(lambda x: x.strip(), cells)
        if rowdata['title'] and rules['tabletitlerowinbold']:
            cells = map(lambda x: enclose_me('fontBold', x), cells)

        # Add cell BEGIN/END tags
        for i, cell in enumerate(cells):
            copen = open_
            cclose = close
            chead = head
            if self.vert_head and i == 0:
                copen = TAGS['tableTitleCellOpen']
                cclose = TAGS['tableTitleCellClose']
                if rules['breaktablecell']:
                    cclose = cclose + '\n'

            # Make sure we will pop from some filled lists
            # Fixes empty line bug '| |'
            this_align = this_cell = this_span = this_mcopen = ''
            if calign:
                this_align = calign.pop(0)
            if caligntag:
                this_cell = caligntag.pop(0)
            if cspan:
                this_span = cspan.pop(0)
            if multicol:
                this_mcopen = multicol.pop(0)

            # Insert cell align into open tag (if cell is alignable)
            if rules['tablecellaligntype'] == 'cell':
                copen = regex['_tableCellAlign'].sub(this_align, copen)
                cclose = regex['_tableCellAlign'].sub(this_align, cclose)
                chead = regex['_tableCellAlign'].sub(this_align, chead)

                # Insert cell data into cellAlign tags
                if this_cell:
                    cell = regex['x'].sub(cell, this_cell)

            # Insert cell span into open tag (if cell is spannable)
            if rules['tablecellspannable']:
                copen = regex['_tableCellColSpan'].sub(this_span, copen)
                cclose = regex['_tableCellColSpan'].sub(this_span, cclose)
                chead = regex['_tableCellColSpan'].sub(this_span, chead)

            # Use multicol tags instead (if multicol supported, and if
            # cell has a span or is aligned differently to column)
            if rules['tablecellmulticol']:
                if this_mcopen:
                    copen = regex['_tableColAlign'].sub(this_align, this_mcopen)
                    cclose = TAGS['_tableCellMulticolClose']

            # RTF target requires the border in each cell
            border = ''
            if self.border:
                border = TAGS['_tableCellBorder']
            copen = regex['_tableBorder'].sub(border, copen)
            cclose = regex['_tableBorder'].sub(border, cclose)
            chead = regex['_tableBorder'].sub(border, chead)

            # Attribute delimiter, added when align/span attributes were used
            # Example: Wikipedia table cell, without and with attributes:
            # | cell contents
            # | align="right" colspan="2" | cell contents
            #
            if regex['_tableAttrDelimiter'].search(copen):
                if this_align or this_span:
                    copen = regex['_tableAttrDelimiter'].sub(TAGS['_tableAttrDelimiter'], copen)
                else:
                    copen = regex['_tableAttrDelimiter'].sub('', copen)  # remove marker

            if chead:
                cellhead.append(chead)
            cellbody.append(copen + cell + cclose)

        # Maybe there are cell separators?
        return ''.join(cellhead) + sep.join(cellbody)

    def add_row(self, cells):
        self.rows.append(cells)

    def parse_row(self, line):
        # Default table properties
        ret = {
            'border': 0, 'title': 0, 'vert_head': 0, 'align': 'Left',
            'cells': [], 'cellalign': [], 'cellspan': []
        }
        # Detect table align (and remove spaces mark)
        if line[0] == ' ':
            ret['align'] = 'Center'
        line = line.lstrip()
        # Detect vertical header mark
        if line[1] == '_':
            ret['vert_head'] = 1
            line = line[0] + line[2:]
        # Detect horizontal and vertical headers mark
        if line[1] == '/':
            ret['vert_head'] = 1
            line = line[0] + '|' + line[2:]
        # Detect title mark
        if line[1] == '|':
            ret['title'] = 1
        # Detect border mark and normalize the EOL
        m = re.search(' (\|+) *$', line)
        if m:
            line = line + ' '
            ret['border'] = 1
        else:
            line = line + ' | '
        # Delete table mark
        line = regex['table'].sub('', line)
        # Detect colspan  | foo | bar baz |||
        line = re.sub(' (\|+)\| ', '\a\\1 | ', line)
        # Split cells (the last is fake)
        ret['cells'] = line.split(' | ')[:-1]
        # Find cells span
        ret['cellspan'] = self._get_cell_span(ret['cells'])
        # Remove span ID
        ret['cells'] = map(lambda x: re.sub('\a\|+$', '', x), ret['cells'])
        # Find cells align
        ret['cellalign'] = self._get_cell_align(ret['cells'])
        # Hooray!
        Debug('Table Prop: %s' % ret, 7)
        return ret

    def dump(self):
        open_ = self._get_full_tag(TAGS['tableOpen'])
        if rules['tablenumber']:
            open_ = re.sub('n_table', str(BLOCK.tablecount), open_)
        if rules['tablecolumnsnumber']:
            open_ = re.sub('n_cols', str(self.n_cols), open_)
        rows  = self.rows
        close = self._get_full_tag(TAGS['tableClose'])

        rowopen     = self._get_full_tag(TAGS['tableRowOpen'])
        rowclose    = self._get_full_tag(TAGS['tableRowClose'])
        rowsep      = self._get_full_tag(TAGS['tableRowSep'])
        titrowopen  = self._get_full_tag(TAGS['tableTitleRowOpen'])  or rowopen
        titrowclose = self._get_full_tag(TAGS['tableTitleRowClose']) or rowclose

        if rules['breaktablelineopen']:
            rowopen = rowopen + '\n'
            titrowopen = titrowopen + '\n'

        # Tex gotchas
        if TARGET in ['tex', 'texs']:
            if not self.border:
                rowopen = titrowopen = ''
            else:
                close = rowopen + close

        # Now we tag all the table cells on each row
        #tagged_cells = map(lambda x: self._tag_cells(x), rows)  #!py15
        tagged_cells = []
        for cell in rows:
            tagged_cells.append(self._tag_cells(cell))

        # Add row separator tags between lines
        tagged_rows = []
        if rowsep:
            #!py15
            #tagged_rows = map(lambda x: x + rowsep, tagged_cells)
            for cell in tagged_cells:
                tagged_rows.append(cell + rowsep)
            # Remove last rowsep, because the table is over
            tagged_rows[-1] = tagged_rows[-1].replace(rowsep, '')
        # Add row BEGIN/END tags for each line
        else:
            for rowdata in rows:
                if rowdata['title']:
                    o, c = titrowopen, titrowclose
                else:
                    o, c = rowopen, rowclose
                row = tagged_cells.pop(0)
                tagged_rows.append(o + row + c)

        # Join the pieces together
        fulltable = []
        if open_:
            fulltable.append(open_)
        fulltable.extend(tagged_rows)
        if close:
            fulltable.append(close)

        return fulltable


##############################################################################


class BlockMaster:
    "TIP: use blockin/out to add/del holders"
    def __init__(self):
        self.BLK = []
        self.HLD = []
        self.PRP = []
        self.depth = 0
        self.count = 0
        self.last = ''
        self.tableparser = None
        self.tablecount = 0
        self.contains = {
            'para'    : ['comment', 'raw', 'tagged'],
            'verb'    : [],
            'table'   : ['comment'],
            'raw'     : [],
            'tagged'  : [],
            'comment' : [],
            'quote'   : ['quote', 'comment', 'raw', 'tagged'],
            'list'    : ['list', 'numlist', 'deflist', 'para', 'verb', 'comment', 'raw', 'tagged'],
            'numlist' : ['list', 'numlist', 'deflist', 'para', 'verb', 'comment', 'raw', 'tagged'],
            'deflist' : ['list', 'numlist', 'deflist', 'para', 'verb', 'comment', 'raw', 'tagged'],
            'bar'     : [],
            'title'   : [],
            'numtitle': [],
        }
        self.allblocks = self.contains.keys()

        # If one is found inside another, ignore the marks
        self.exclusive = ['comment', 'verb', 'raw', 'tagged']

        # May we include bars inside quotes?
        if rules['barinsidequote']:
            self.contains['quote'].append('bar')

    def block(self):
        if not self.BLK:
            return ''
        return self.BLK[-1]

    def isblock(self, name=''):
        return self.block() == name

    def prop(self, key):
        if not self.PRP:
            return ''
        return self.PRP[-1].get(key) or ''

    def propset(self, key, val):
        self.PRP[-1][key] = val
        #Debug('BLOCK prop ++: %s->%s' % (key, repr(val)), 1)
        #Debug('BLOCK props: %s' % (repr(self.PRP)), 1)

    def hold(self):
        if not self.HLD:
            return []
        return self.HLD[-1]

    def holdadd(self, line):
        if self.block().endswith('list'):
            line = [line]
        self.HLD[-1].append(line)
        Debug('HOLD add: %s' % repr(line), 4)
        Debug('FULL HOLD: %s' % self.HLD, 4)

    def holdaddsub(self, line):
        self.HLD[-1][-1].append(line)
        Debug('HOLD addsub: %s' % repr(line), 4)
        Debug('FULL HOLD: %s' % self.HLD, 4)

    def holdextend(self, lines):
        if self.block().endswith('list'):
            lines = [lines]
        self.HLD[-1].extend(lines)
        Debug('HOLD extend: %s' % repr(lines), 4)
        Debug('FULL HOLD: %s' % self.HLD, 4)

    def blockin(self, block):
        ret = []
        if block not in self.allblocks:
            Error("Invalid block '%s'" % block)

        # First, let's close other possible open blocks
        while self.block() and block not in self.contains[self.block()]:
            ret.extend(self.blockout())

        # Now we can gladly add this new one
        self.BLK.append(block)
        self.HLD.append([])
        self.PRP.append({})
        self.count += 1
        if block == 'table':
            self.tableparser = TableMaster()
        # Deeper and deeper
        self.depth = len(self.BLK)
        Debug('block ++ (%s): %s' % (block, self.BLK), 3)
        return ret

    def blockout(self):
        if not self.BLK:
            Error('No block to pop')
        blockname = self.BLK.pop()
        result = getattr(self, blockname)()
        parsed = self.HLD.pop()
        self.PRP.pop()
        self.depth = len(self.BLK)

        if rules['tableonly'] and blockname != 'table':
            return []

        if blockname == 'table':
            del self.tableparser

        # Inserting a nested block into mother
        if self.block():
            if blockname != 'comment':  # ignore comment blocks
                if self.block().endswith('list'):
                    self.HLD[-1][-1].append(result)
                else:
                    self.HLD[-1].append(result)
            # Reset now. Mother block will have it all
            result = []

        Debug('block -- (%s): %s' % (blockname, self.BLK), 3)
        Debug('RELEASED (%s): %s' % (blockname, parsed), 3)

        # Save this top level block name (produced output)
        # The next block will use it
        if result:
            self.last = blockname
            if rules['iswrapped']:
                final = []
                if not rules['asciiart']:
                    for line in result:
                        if not line or blockname in ('verb', 'tagged'):
                            final.append(line)
                        else:
                            final.extend(aa.wrap(line, CONF['width'], False))
                elif rules['slides'] and blockname in ('list', 'numlist', 'deflist'):
                    final.extend(aa.box(result, AA, CONF['width'], False, web=rules['web'], slides=rules['slides']))
                else:
                    for line in result:
                        if not line or (blockname in ('table', 'tagged', 'verb') and not rules['slides']) or (blockname == 'quote' and rules['slides']):
                            final.append(line)
                        else:
                            if rules['slides'] and blockname in ('table', 'tagged', 'verb'):
                                final.append(line[:CONF['width']])
                            elif rules['slides']:
                                final.extend(' ' + line for line in aa.wrap(line, CONF['width'] - 2, rules['web']))
                            else:
                                final.extend(aa.wrap(line, CONF['width'], rules['web']))
                result = final[:]

            Debug('BLOCK: %s' % result, 6)

        # ASCII Art processing
        global AA_TITLE, AA_COUNT, AA_PW_TOC, AA_IMG
        if rules['asciiart'] and rules['slides'] and not CONF['toc-only'] and not CONF.get('art-no-title'):
            len_res = len(result)
            for el in result:
                if '<img' in el:
                    len_res = len_res + AA_IMG
            n = (CONF['height'] - 1) - (AA_COUNT % (CONF['height'] - 1) + 1)
            if n < len_res and not (TITLE.level == 1 and blockname in ["title", "numtitle"]):
                if rules['web']:
                    if len_res > CONF['height']:
                        result[CONF['height'] - 8] = result[CONF['height'] - 8] + '</pre></section>'
                        for i, line in enumerate(result[CONF['height'] - 7:]):
                            j = i % (CONF['height'] - 1)
                            if j == 0:
                                result[i + CONF['height'] - 7] = '<section><pre>' + line
                            elif j == CONF['height'] - 2:
                                result[i + CONF['height'] - 7] = line + '</pre></section>'
                    result = ([''] * n) + [aa.line(AA['bar1'], CONF['width']) + '</pre></section>'] + aa.slide(AA_TITLE, AA['bar2'], CONF['width'], True) + [''] + result
                else:
                    result = ([''] * n) + [aa.line(AA['bar1'], CONF['width'])] + aa.slide(AA_TITLE, AA['bar2'], CONF['width'], False) + [''] + result
            if (blockname in ["title", "numtitle"] and TITLE.level == 1) or not AA_TITLE:
                if not AA_TITLE:
                    if CONF['headers']:
                        AA_TITLE = CONF['header1'] or ' '
                    else:
                        AA_TITLE = ' '
                aa_title = aa.slide(AA_TITLE, AA['bar2'], CONF['width'], rules['web']) + ['']
                if AA_COUNT:
                    if rules['web']:
                        aa_title = ([''] * n) + [aa.line(AA['bar2'], CONF['width']) + '</pre></section>'] + aa_title
                    else:
                        aa_title = ([''] * n) + [aa.line(AA['bar2'], CONF['width'])] + aa_title
                result = aa_title + result
            AA_COUNT += len(result) + AA_IMG
            AA_IMG = 0
            if blockname in ["title", "numtitle"]:
                AA_PW_TOC[TITLE.txt] = AA_COUNT

        return result

    def _last_escapes(self, line):
        return doFinalEscape(TARGET, line)

    def _get_escaped_hold(self):
        ret = []
        for line in self.hold():
            linetype = type(line)
            if linetype == type('') or linetype == type(u''):
                ret.append(self._last_escapes(line))
            elif linetype == type([]):
                ret.extend(line)
            else:
                Error("BlockMaster: Unknown HOLD item type: %s" % linetype)
        return ret

    def _remove_twoblanks(self, lastitem):
        if len(lastitem) > 1 and lastitem[-2:] == ['', '']:
            return lastitem[:-2]
        return lastitem

    def _should_add_blank_line(self, where, blockname):
        "Validates the blanksaround* rules"

        # Nestable blocks: only mother blocks (level 1) are spaced
        if blockname.endswith('list') and self.depth > 1:
            return False

        # The blank line after the block is always added
        if where == 'after' \
            and rules['blanksaround' + blockname]:
            return True

        # # No blank before if it's the first block of the body
        # elif where == 'before' \
        #   and BLOCK.count == 1:
        #   return False

        # # No blank before if it's the first block of this level (nested)
        # elif where == 'before' \
        #   and self.count == 1:
        #   return False

        # The blank line before the block is only added if
        # the previous block haven't added a blank line
        # (to avoid consecutive blanks)
        elif where == 'before' \
            and rules['blanksaround' + blockname] \
            and not rules.get('blanksaround' + self.last):
            return True

        # Nested quotes are handled here,
        # because the mother quote isn't closed yet
        elif where == 'before' \
            and blockname == 'quote' \
            and rules['blanksaround' + blockname] \
            and self.depth > 1:
            return True

        return False

    # functions to help encode block depth into RTF formatting
    def _apply_depth(self, line, level):
        # convert block depth into an indent in twips
        depth = level
        multiply = rules['blockdepthmultiply']
        if depth > 0 and rules['depthmultiplyplus']:
            depth = depth + rules['depthmultiplyplus']
        if multiply:
            depth = depth * multiply
        return regex['_blockDepth'].sub(str(depth), line)

    def _apply_list_level(self, line, level):
        mylevel = level
        if rules['listlevelzerobased']:
            mylevel = mylevel - 1
        return regex['_listLevel'].sub(str(mylevel), line)

    def comment(self):
        return ''

    def raw(self):
        lines = self.hold()
        return map(lambda x: doEscape(TARGET, x), lines)

    def tagged(self):
        return self.hold()

    def para(self):
        result = []
        open_ = TAGS['paragraphOpen']
        close = TAGS['paragraphClose']
        lines = self._get_escaped_hold()

        # Blank line before?
        if self._should_add_blank_line('before', 'para'):
            result.append('')

        # RTF needs depth level encoded into nested paragraphs
        mydepth = self.depth
        if rules['zerodepthparagraph']:
            mydepth = 0
        open_ = self._apply_depth(open_, mydepth)

        # Open tag
        if open_:
            result.append(open_)

        # Pagemaker likes a paragraph as a single long line
        if rules['onelinepara']:
            result.append(' '.join(lines))
        # Others are normal :)
        else:
            result.extend(lines)

        # Close tag
        if close:
            result.append(close)

        # Blank line after?
        if self._should_add_blank_line('after', 'para'):
            result.append('')

        # Very very very very very very very very very UGLY fix
        # Needed because <center> can't appear inside <p>
        try:
            if len(lines) == 1 and \
               TARGET in ('html', 'xhtml', 'xhtmls', 'wp') and \
               re.match('^\s*<center>.*</center>\s*$', lines[0]):
                result = [lines[0]]
        except:
            pass

        return result

    def verb(self):
        "Verbatim lines are not masked, so there's no need to unmask"
        result = []
        open_ = TAGS['blockVerbOpen']
        close = TAGS['blockVerbClose']
        sep = TAGS['blockVerbSep']

        # Blank line before?
        if self._should_add_blank_line('before', 'verb'):
            result.append('')

        # Open tag
        if open_:
            result.append(open_)

        # Get contents
        for line in self.hold():
            if self.prop('mapped') == 'table':
                line = MacroMaster().expand(line)
            if not rules['verbblocknotescaped']:
                line = doEscape(TARGET, line)
            if TAGS['blockVerbLine']:
                line = TAGS['blockVerbLine'] + line
            if rules['indentverbblock']:
                line = '  ' + line
            if rules['verbblockfinalescape']:
                line = doFinalEscape(TARGET, line)
            result.append(line)
            if sep:
                result.append(sep)

        if sep:
            result.pop()

        # Close tag
        if close:
            result.append(close)

        # Blank line after?
        if self._should_add_blank_line('after', 'verb'):
            result.append('')

        return result

    def numtitle(self):
        return self.title('numtitle')

    def title(self, name='title'):
        result = []

        # Blank line before?
        if self._should_add_blank_line('before', name):
            result.append('')

        # Get contents
        result.extend(TITLE.get())

        # Blank line after?
        if self._should_add_blank_line('after', name):
            result.append('')

        return result

    def table(self):
        self.tablecount += 1
        result = []

        if rules['asciiart'] and self.tableparser.rows:
            if rules['spread']:
                data = spread.spreadsheet(completes_table(self.tableparser.rows), rules['spreadmarkup'], rules['spreadgrid'])
                return aa.table(data, AA, CONF['width'], True, True, True, 'Center', True, rules['web']) + ['']
            else:
                return aa.table(completes_table(self.tableparser.rows), AA, CONF['width'], self.tableparser.border, self.tableparser.title, self.tableparser.vert_head, self.tableparser.align, False, False) + ['']

        if TARGET == 'rst' and self.tableparser.rows:
            chars = AA.copy()
            if not self.tableparser.border:
                chars['border'] = '='
                chars['tlcorner'] = chars['trcorner'] = chars['cross'] = chars['blcorner'] = chars['brcorner'] = chars['lcross'] = chars['side'] = chars['rcross'] = chars['tcross'] = chars['bcross'] = chars['lhhead'] = chars['rhhead'] = ' '
            return aa.table(completes_table(self.tableparser.rows), chars, CONF['width'], self.tableparser.border, self.tableparser.title, False, 'Left', False, False) + ['']

        if TARGET == 'mgp' and self.tableparser.rows:
            aa_t = aa.table(completes_table(self.tableparser.rows), AA, CONF['width'], True, self.tableparser.title, False, 'Left', False, False)
            try:
                import aafigure
                t_name = 'table_' + str(self.tablecount) + '.png'
                aafigure.render(unicode('\n'.join(aa_t)), t_name, {'format':'png', 'background':'#000000', 'foreground':'#FFFFFF', 'textual':True})
                return ['%center', '%newimage "' + t_name + '"']
            except:
                return ['%font "mono"'] + aa_t + ['']

        if TARGET == 'db' and self.tableparser.rows:
            data = completes_table(self.tableparser.rows)
            n = max([len(line[0]) for line in data])
            table = 'table_' + str(self.tablecount)
            if self.tableparser.title:
                cols = [s.strip().replace(' ', '_') for s in data[0][0]]
                del data[0]
            else:
                cols = []
                for i in range(n):
                    cols.append('col_' + str(i + 1))
            cols_insert = ', '.join(cols)
            cols_create = ' text, '.join(cols) + ' text'
            sql_create = 'create table ' + table + ' (id integer primary key, ' +  cols_create + ')'
            DBC.execute(sql_create)
            sql_insert = 'insert into ' + table + ' (' +  cols_insert + ') values' + ' (' + ('?,' * n)[:-1] + ')'
            for line in data:
                DBC.execute(sql_insert, line[0])
            DB.commit()

        # Blank line before?
        if self._should_add_blank_line('before', 'table'):
            result.append('')

        # Rewrite all table cells by the unmasked and escaped data
        lines = self._get_escaped_hold()
        for i in xrange(len(lines)):
            cells = lines[i].split(SEPARATOR)
            self.tableparser.rows[i]['cells'] = cells
        if rules['spread']:
            data = spread.spreadsheet(completes_table(self.tableparser.rows), rules['spreadmarkup'], rules['spreadgrid'])
            self.tableparser.border, len_line = True, len(data[0][0])
            self.tableparser.cellalign = len_line
            self.tableparser.colalign = len_line * ['Left']
            if rules['spreadgrid']:
                self.tableparser.vert_head = True
                self.tableparser.rows = [{'cells':data[0][0], 'cellspan':data[0][1], 'cellalign':['Left']*len_line, 'title':1}] + self.tableparser.rows
                for i, row in enumerate(self.tableparser.rows[1:]):
                    row['cells'], row['cellspan'], row['cellalign'], row['title'] = data[i+1][0], data[i+1][1], ['Left']*len_line, 0
            else:
                for i, row in enumerate(self.tableparser.rows):
                    row['cells'], row['cellspan'], row['cellalign'], row['title'] = data[i][0], data[i][1], ['Left']*len_line, 0
        result.extend(self.tableparser.dump())

        # Blank line after?
        if self._should_add_blank_line('after', 'table'):
            result.append('')

        return result

    def quote(self):
        result = []
        open_  = TAGS['blockQuoteOpen']            # block based
        close  = TAGS['blockQuoteClose']
        qline  = TAGS['blockQuoteLine']            # line based
        indent = tagindent = '\t' * self.depth

        # Apply rules
        if rules['tagnotindentable']:
            tagindent = ''
        if not rules['keepquoteindent']:
            indent = ''

        # Blank line before?
        if self._should_add_blank_line('before', 'quote'):
            result.append('')

        # RTF needs depth level encoded into almost everything
        open_ = self._apply_depth(open_, self.depth)

        # Open tag
        if open_:
            result.append(tagindent + open_)

        itemisclosed = False

        # Get contents
        if rules['onelinequote']:
            # XXX Dirty hack, won't work for nested blocks inside quote (when TABS are used in your t2t source), even subquotes
            result.append(' '.join([regex['quote'].sub('', x) for x in self.hold()]))
        else:
            for item in self.hold():
                if type(item) == type([]):
                    if close and rules['quotenotnested']:
                        result.append(tagindent + close)
                        itemisclosed = True
                    result.extend(item)        # subquotes
                else:
                    if open_ and itemisclosed:
                        result.append(tagindent + open_)
                    item = regex['quote'].sub('', item)  # del TABs
                    item = self._last_escapes(item)
                    if rules['asciiart'] and rules['slides']:
                        result.extend(aa.box([item], AA, CONF['width'], web=rules['web'], slides=rules['slides']))
                    else:
                        item = qline * self.depth + item
                        result.append(indent + item)  # quote line

        # Close tag
        if close and not itemisclosed:
            result.append(tagindent + close)

        # Blank line after?
        if self._should_add_blank_line('after', 'quote'):
            result.append('')

        return result

    def bar(self):
        result = []
        bar_tag = ''

        # Blank line before?
        if self._should_add_blank_line('before', 'bar'):
            result.append('')

        # Get the original bar chars
        bar_chars = self.hold()[0].strip()

        # Set bar type
        if bar_chars.startswith('='):
            bar_tag = TAGS['bar2']
        else:
            bar_tag = TAGS['bar1']

        # To avoid comment tag confusion like <!-- ------ --> (sgml)
        if TAGS['comment'].count('--'):
            bar_chars = bar_chars.replace('--', '__')

        # Get the bar tag (may contain \a)
        result.append(regex['x'].sub(bar_chars, bar_tag))

        # Blank line after?
        if self._should_add_blank_line('after', 'bar'):
            result.append('')

        return result

    def deflist(self):
        return self.list('deflist')

    def numlist(self):
        return self.list('numlist')

    def list(self, name='list'):
        result    = []
        items     = self.hold()
        indent    = self.prop('indent')
        tagindent = indent
        listline  = TAGS.get(name + 'ItemLine')
        itemcount = 0

        if name == 'deflist':
            itemopen  = TAGS[name + 'Item1Open']
            itemclose = TAGS[name + 'Item2Close']
            itemsep   = TAGS[name + 'Item1Close'] +\
                        TAGS[name + 'Item2Open']
        else:
            itemopen  = TAGS[name + 'ItemOpen']
            itemclose = TAGS[name + 'ItemClose']
            itemsep   = ''

        # Apply rules
        if rules['tagnotindentable']:
            tagindent = ''
        if not rules['keeplistindent']:
            indent = tagindent = ''

        # RTF encoding depth
        itemopen = self._apply_depth(itemopen, self.depth)
        itemopen = self._apply_list_level(itemopen, self.depth)

        # ItemLine: number of leading chars identifies list depth
        if listline:
            if rules['listlineafteropen']:
                itemopen  = itemopen + listline * self.depth
            else:
                itemopen  = listline * self.depth + itemopen

        # Adds trailing space on opening tags
        if (name == 'list'    and rules['spacedlistitemopen']) or \
           (name == 'numlist' and rules['spacednumlistitemopen']):
            itemopen = itemopen + ' '

        # Remove two-blanks from list ending mark, to avoid <p>
        items[-1] = self._remove_twoblanks(items[-1])

        # Blank line before?
        if self._should_add_blank_line('before', name):
            result.append('')

        if rules['blanksaroundnestedlist']:
            result.append('')

        # Tag each list item (multiline items), store in listbody
        itemopenorig = itemopen
        listbody = []
        widelist = 0
        for item in items:

            # Add "manual" item count for noautonum targets
            itemcount += 1
            if name == 'numlist' and not rules['autonumberlist']:
                n = str(itemcount)
                itemopen = regex['x'].sub(n, itemopenorig)
                del n

            # Tag it
            item[0] = self._last_escapes(item[0])
            if name == 'deflist':
                z, term, rest = item[0].split(SEPARATOR, 2)
                item[0] = rest
                if not item[0]:
                    del item[0]      # to avoid <p>
                listbody.append(tagindent + itemopen + term + itemsep)
            else:
                fullitem = tagindent + itemopen
                listbody.append(item[0].replace(SEPARATOR, fullitem))
                del item[0]

            itemisclosed = False

            # Process next lines for this item (if any)
            for line in item:
                if type(line) == type([]):  # sublist inside
                    if rules['listitemnotnested'] and itemclose:
                        listbody.append(tagindent + itemclose)
                        itemisclosed = True
                    if TARGET == 'rst' and name == 'deflist':
                        del line[0]
                    listbody.extend(line)
                else:
                    line = self._last_escapes(line)

                    # Blank lines turns to <p>
                    if not line and rules['parainsidelist']:
                        line = indent + TAGS['paragraphOpen'] + TAGS['paragraphClose']
                        line = line.rstrip()
                        widelist = 1
                    elif not line and TARGET == 'rtf':
                        listbody.append(TAGS['paragraphClose'])
                        line = TAGS['paragraphOpen']
                        line = self._apply_depth(line, self.depth)

                    # Some targets don't like identation here (wiki)
                    if not rules['keeplistindent'] or (name == 'deflist' and rules['deflisttextstrip']):
                        line = line.lstrip()

                    # Maybe we have a line prefix to add? (wiki)
                    if name == 'deflist' and TAGS['deflistItem2LinePrefix']:
                        line = TAGS['deflistItem2LinePrefix'] + line

                    listbody.append(line)

            # Close item (if needed)
            if itemclose and not itemisclosed:
                if rules['notbreaklistitemclose']:
                    listbody[-1] += itemclose
                else:
                    listbody.append(tagindent + itemclose)

        if not widelist and rules['compactlist']:
            listopen = TAGS.get(name + 'OpenCompact')
            listclose = TAGS.get(name + 'CloseCompact')
        else:
            listopen  = TAGS.get(name + 'Open')
            listclose = TAGS.get(name + 'Close')

        # Open list (not nestable lists are only opened at mother)
        if listopen and not \
           (rules['listnotnested'] and BLOCK.depth != 1):
            result.append(tagindent + listopen)

        result.extend(listbody)

        # Close list (not nestable lists are only closed at mother)
        if listclose and not \
           (rules['listnotnested'] and self.depth != 1):
            result.append(tagindent + listclose)

        # Blank line after?
        if self._should_add_blank_line('after', name):
            result.append('')

        if rules['blanksaroundnestedlist']:
            if result[-1]:
                result.append('')

        return result


##############################################################################


class MacroMaster:
    def __init__(self, config={}):
        self.name     = ''
        self.config   = config or CONF
        self.infile   = self.config['sourcefile']
        self.outfile  = self.config['outfile']
        self.currentfile = self.config['currentsourcefile']
        self.currdate = time.localtime(time.time())
        self.rgx      = regex.get('macros') or getRegexes()['macros']
        self.fileinfo = {'infile': None, 'outfile': None}
        self.dft_fmt  = MACROS

    def walk_file_format(self, fmt):
        "Walks the %%{in/out}file format string, expanding the % flags"
        i = 0
        ret = ''
        while i < len(fmt):                     # char by char
            c = fmt[i]
            i += 1
            if c == '%':                        # hot char!
                if i == len(fmt):               # % at the end
                    ret = ret + c
                    break
                c = fmt[i]                      # read next
                i += 1
                ret = ret + self.expand_file_flag(c)
            else:
                ret = ret + c                   # common char
        return ret

    def expand_file_flag(self, flag):
        "%f: filename          %F: filename (w/o extension)"
        "%d: dirname           %D: dirname (only parent dir)"
        "%p: file path         %e: extension"
        info = self.fileinfo[self.name]         # get dict
        if   flag == '%':
            x = '%'                             # %% -> %
        elif flag == 'f':
            x = info['name']
        elif flag == 'F':
            x = os.path.splitext(info['name'])[0]
        elif flag == 'd':
            x = info['dir']
        elif flag == 'D':
            x = os.path.split(info['dir'])[-1]
        elif flag == 'p':
            x = info['path']
        elif flag == 'e':
            x = os.path.splitext(info['name'])[1].replace('.', '')
        else:
            x = '%' + flag                      # false alarm
        return x

    def set_file_info(self, macroname):
        if (macroname == 'currentfile'):
            self.currentfile = self.config['currentsourcefile']
        else:
            if self.fileinfo.get(macroname):    # already done
                return
        file_ = getattr(self, self.name)        # self.infile
        if file_ == STDOUT or file_ == MODULEOUT:
            dir_ = ''
            path = name = file_
        else:
            path = os.path.abspath(file_)
            dir_ = os.path.dirname(path)
            name = os.path.basename(path)
        self.fileinfo[macroname] = {'path': path, 'dir': dir_, 'name': name}

    def expand(self, line=''):
        "Expand all macros found on the line"
        while self.rgx.search(line):
            m = self.rgx.search(line)
            name = self.name = m.group('name').lower()
            fmt = m.group('fmt') or self.dft_fmt.get(name)
            if name == 'date':
                txt = time.strftime(fmt, self.currdate)
            elif name == 'mtime':
                if self.infile in (STDIN, MODULEIN):
                    fdate = self.currdate
                elif PathMaster().is_url(self.infile):
                    try:
                        # Doing it the easy way: fetching the URL again.
                        # The right way would be doing it in Readfile().
                        # But I'm trying to avoid yet another global var
                        # or fake 'sourcefile_mtime' config.
                        #
                        # >>> f= urllib.urlopen('http://txt2tags.org/index.t2t')
                        # >>> f.info().get('last-modified')
                        # 'Thu, 18 Nov 2010 22:42:11 GMT'
                        # >>>
                        #
                        from urllib import urlopen
                        from email.Utils import parsedate

                        f = urlopen(self.infile)
                        mtime_rfc2822 = f.info().get('last-modified')
                        fdate = parsedate(mtime_rfc2822)
                    except:
                        # If mtime cannot be found, defaults to current date
                        fdate = self.currdate
                else:
                    mtime = os.path.getmtime(self.infile)
                    fdate = time.localtime(mtime)
                txt = time.strftime(fmt, fdate)
            elif name in ('infile', 'outfile', 'currentfile'):
                self.set_file_info(name)
                txt = self.walk_file_format(fmt)
            elif name == 'appurl':
                txt = my_url
            elif name == 'appname':
                txt = my_name
            elif name == 'appversion':
                txt = my_version
            elif name == 'target':
                txt = TARGET
            elif name == 'encoding':
                txt = self.config['encoding']
            elif name == 'cmdline':
                txt = '%s %s' % (my_name, ' '.join(self.config['realcmdline']))
            elif name in ('header1', 'header2', 'header3'):
                txt = self.config[name]
            elif name == 'cc':
                txt = cc_formatter(self.config, fmt)
            else:
                # Never reached because the macro regex list the valid keys
                Error("Unknown macro name '%s'" % name)
            line = self.rgx.sub(txt, line, 1)
        return line


##############################################################################

def cc_formatter(conf, size):
    cc, target = conf['cc'].lower(), conf['target']
    licenses = 'by, by-sa, by-nc-sa, by-nd, by-nc-nd, by-nc'
    if cc not in licenses.split(', '):
        Error(_('Please, choose one of the six valid Creative Commons licenses : %s.') % licenses)
    if target in ('html', 'xhtml', 'xhtmls', 'html5') or (rules['asciiart'] and rules['web']):
        if size == 'small':
            end_img = '/3.0/80x15.png'
        else:
            end_img = '/3.0/88x31.png'
        url = 'http://creativecommons.org/licenses/' + cc + '/3.0'
        img = 'http://i.creativecommons.org/l/' + cc + end_img
        alt = 'Creative Commons ' + cc
        ret = '<a href="' + url + '"><img src="' + img + '" alt="' + alt + '"></a>'
    else:
        if size == 'small':
            ret = 'Creative Commons %s' % cc
        else:
            ret = 'Creative Commons %s' % cc.upper()
    return ret


def listTargets():
    """list all available targets"""
    for typ in TARGET_TYPES:
        targets = list(TARGET_TYPES[typ][1])
        targets.sort()
        print
        print TARGET_TYPES[typ][0] + ':'
        for target in targets:
            print "\t%s\t%s" % (target, TARGET_NAMES.get(target))
    if OTHER_TARGETS:
        print
        print _('OTHERS:')
        for target in OTHER_TARGETS:
            print "\t%s\t%s" % (target, TARGET_NAMES.get(target))
    print


def dumpConfig(source_raw, parsed_config):
    onoff = {1: _('ON'), 0: _('OFF')}
    data = [
        (_('RC file')        , RC_RAW     ),
        (_('source document'), source_raw ),
        (_('command line')   , CMDLINE_RAW)
    ]
    # First show all RAW data found
    for label, cfg in data:
        print _('RAW config for %s') % label
        for target, key, val in cfg:
            target = '(%s)' % target
            key    = dotted_spaces("%-14s" % key)
            val    = val or _('ON')
            print '  %-8s %s: %s' % (target, key, val)
        print
    # Then the parsed results of all of them
    print _('Full PARSED config')
    keys = parsed_config.keys()
    keys.sort()  # sorted
    for key in keys:
        val = parsed_config[key]
        # Filters are the last
        if key in ['preproc', 'postproc', 'postvoodoo']:
            continue
        # Flag beautifier
        if key in FLAGS or key in ACTIONS:
            val = onoff.get(val) or val
        # List beautifier
        if type(val) == type([]):
            if key == 'options':
                sep = ' '
            else:
                sep = ', '
            val = sep.join(val)
        print "%25s: %s" % (dotted_spaces("%-14s" % key), val)
    print
    print _('Active filters')
    for filter_ in ['preproc', 'postproc', 'postvoodoo']:
        for rule in parsed_config.get(filter_) or []:
            print "%25s: %s  ->  %s" % (
                dotted_spaces("%-14s" % filter_), rule[0], rule[1])


def get_file_body(file_):
    "Returns all the document BODY lines"
    return process_source_file(file_, noconf=1)[1][2]


def post_voodoo(lines, config):
    r'''
    %!postvoodoo handler - Beware! Voodoo here. For advanced users only.

    Your entire output document will be put in a single string, to your
    search/replace pleasure. Line breaks are single \n's in all platforms.
    You can change multiple lines at once, or even delete them. This is the
    last txt2tags processing in your file. All %!postproc's were already
    applied. It's the same as:

        $ txt2tags myfile.t2t | postvoodoo

    Your regex will be compiled with no modifiers. The default behavior is:

        ^ and $ match begin/end of entire string
        . doesn't match \n
        \w is not locale aware
        \w is not Unicode aware

    You can use (?...) in the beginning of your regex to change behavior:

        (?s)    the dot . will match \n, so .* will get everything
        (?m)    the ^ and $ match begin/end of EACH inner line
        (?u)    the \w, \d, \s and friends will be Unicode aware

    You can also use (?smu) or any combination of those.
    Learn more in http://docs.python.org/library/re.html
    '''

    loser1 = _('No, no. Your PostVoodoo regex is wrong. Maybe you should call mommy?')
    loser2 = _('Dear PostVoodoo apprentice: You got the regex right, but messed the replacement')

    subject = '\n'.join(lines)
    spells = compile_filters(config['postvoodoo'], loser1)

    for (magic, words) in spells:
        try:
            subject = magic.sub(words, subject)
        except:
            Error("%s: '%s'" % (loser2, words))

    return subject.split('\n')


def finish_him(outlist, config):
    "Writing output to screen or file"
    outfile = config['outfile']
    file_dict = {}
    outlist = unmaskEscapeChar(outlist)
    outlist = expandLineBreaks(outlist)

    # Apply PostProc filters
    if config['postproc']:
        filters = compile_filters(config['postproc'],
            _('Invalid PostProc filter regex'))
        postoutlist = []
        errmsg = _('Invalid PostProc filter replacement')
        for line in outlist:
            for rgx, repl in filters:
                try:
                    line = rgx.sub(repl, line)
                except:
                    Error("%s: '%s'" % (errmsg, repl))
            postoutlist.append(line)
        outlist = postoutlist[:]

    if config['postvoodoo']:
        outlist = post_voodoo(outlist, config)

    if MAILING and not rules['tableonly']:
        reader = MAILING
        repl_dict = {}
        for i, val in enumerate(reader):
            if i == 0:
                for j, el in enumerate(val):
                    repl_dict[el] = j
            else:     
                write_file = outfile
                for key in repl_dict:
                    write_file = write_file.replace('<<%s>>' % key, val[repl_dict[key]])
                point = write_file.rfind('.')
                if write_file == outfile or write_file in file_dict:
                    if point == -1:
                        write_file = write_file + '_' + str(i)
                    else:
                        write_file = write_file[:point] + '_' + str(i) + write_file[point:]
                newout = []
                for line in outlist:
                    for key in repl_dict:
                        line = line.replace('<<%s>>' % key, val[repl_dict[key]])
                    newout.append(line)
                file_dict[write_file] = newout
    else:
        file_dict[outfile] = outlist

    if config['target'] == 'db':
        DBC.close()
        if outfile in [MODULEOUT, STDOUT]:
            outlist = [open(config['outfile']).read()]
            os.remove(config['outfile'])

    outlist = []
    if outfile == MODULEOUT:
        for write_file in file_dict:
            outlist.append(file_dict[write_file])
        return outlist
    elif outfile == STDOUT:
        for write_file in file_dict:
            outlist.extend(file_dict[write_file])
        if GUI:
            return outlist, config
        else:
            for line in outlist:
                if isinstance(line, unicode):
                    line = line.encode('utf-8')
                print line
    else:
        if not config['target'] == 'db':
            for write_file in file_dict:
                Savefile(write_file, addLineBreaks(file_dict[write_file]))
        if not GUI and not QUIET:
            for write_file in file_dict:
                print _('%s wrote %s') % (my_name, write_file)

    if config['split']:
        if not QUIET:
            print "--- html..."
        sgml2html = 'sgml2html -s %s -l %s %s' % (
            config['split'], config['lang'] or lang, outfile)
        if not QUIET:
            print "Running system command:", sgml2html
        os.system(sgml2html)


def toc_inside_body(body, toc, config):
    ret = []
    if AUTOTOC:
        return body                     # nothing to expand
    toc_mark = MaskMaster().tocmask
    # Expand toc mark with TOC contents
    flag, n = False, 0
    for i,line in enumerate(body):
        if line.count(toc_mark):            # toc mark found
            if config['toc']:
                if rules['asciiart'] and rules['slides']:
                    j = i % (config['height'] - 1)
                    title = body[i - j + 2 + n]
                    ret.extend([''] * (config['height'] - j - 2 + n))
                    ret.extend([aa.line(AA['bar1'], config['width'])] + toc + aa.slide(title, AA['bar2'], config['width'], rules['web']) + [''])
                    flag = True
                else:
                    ret.extend(toc)     # include if --toc
            else:
                pass                # or remove %%toc line
        else:
            if flag and rules['asciiart'] and rules['slides'] and body[i] == body[i + 4] == aa.line(AA['bar2'], config['width']):
                end = [ret[-1]]
                del ret[-1]
                ret.extend([''] * (j - 6 - n) + end)
                flag, n = False, n + 1
                ret.append(line)            # common line
            else:
                ret.append(line)            # common line
    return ret


def toc_tagger(toc, config):
    "Returns the tagged TOC, as a single tag or a tagged list"
    ret = []
    # Convert the TOC list (t2t-marked) to the target's list format
    if config['toc-only'] or (config['toc'] and not TAGS['TOC']):
        fakeconf = config.copy()
        fakeconf['headers']    = 0
        fakeconf['toc-only']   = 0
        fakeconf['mask-email'] = 0
        fakeconf['preproc']    = []
        fakeconf['postproc']   = []
        fakeconf['postvoodoo'] = []
        fakeconf['css-sugar']  = 0
        fakeconf['fix-path']   = 0
        fakeconf['art-no-title']  = 1  # needed for --toc and --slides together, avoids slide title before TOC
        ret, foo = convert(toc, fakeconf)
        set_global_config(config)   # restore config
    # Our TOC list is not needed, the target already knows how to do a TOC
    elif config['toc'] and TAGS['TOC']:
        ret = [TAGS['TOC']]
    return ret


def toc_formatter(toc, config):
    "Formats TOC for automatic placement between headers and body"

    if config['toc-only']:
        return toc              # no formatting needed
    if not config['toc']:
        return []               # TOC disabled
    ret = toc

    # Art: An automatic "Table of Contents" header is added to the TOC slide
    if rules['asciiart'] and rules['slides']:
        n = (config['height'] - 1) - (len(toc) + 6) % (config['height'] - 1)
        toc = aa.slide(config['toc-title'] or _("Table of Contents"), AA['bar2'], config['width'], rules['web']) + toc + ([''] * n)
        end_toc = aa.line(AA['bar2'], config['width'])
        if rules['web']:
            end_toc = end_toc + '</pre></section>'
        toc.append(end_toc)
        return toc
    if rules['asciiart'] and not rules['slides']:
        ret = aa.box([config['toc-title'] or _("Table of Contents")], AA, config['width']) + toc

    # TOC open/close tags (if any)
    if TAGS['tocOpen']:
        ret.insert(0, TAGS['tocOpen'])
    if TAGS['tocClose']:
        ret.append(TAGS['tocClose'])

    # Autotoc specific formatting
    if AUTOTOC:
        if rules['autotocwithbars']:           # TOC between bars
            para = TAGS['paragraphOpen'] + TAGS['paragraphClose']
            bar  = regex['x'].sub('-' * DFT_TEXT_WIDTH, TAGS['bar1'])
            tocbar = [para, bar, para]
            if rules['asciiart'] and config['headers']:
                # exception: header already printed a bar
                ret = [para] + ret + tocbar
            else:
                ret = tocbar + ret + tocbar
        if rules['blankendautotoc']:           # blank line after TOC
            ret.append('')
        if rules['autotocnewpagebefore']:      # page break before TOC
            ret.insert(0, TAGS['pageBreak'])
        if rules['autotocnewpageafter']:       # page break after TOC
            ret.append(TAGS['pageBreak'])
    return ret


# XXX change function name. Now it's called at the end of the execution, dumping the full template.
def doHeader(headers, config):
    if not config['headers']:
        return config['fullBody']
    if not headers:
        headers = ['', '', '']
    target = config['target']
    if target not in HEADER_TEMPLATE:
        Error("doHeader: Unknown target '%s'" % target)

    # Use default templates
    if config['template'] == '' :
        if target in ('html', 'xhtml', 'xhtmls', 'html5') and config.get('css-sugar'):
            template = HEADER_TEMPLATE[target + 'css'].split('\n')
        else:
            template = HEADER_TEMPLATE[target].split('\n')

        template.append('%(BODY)s')

        if TAGS['EOD']:
            template.append(TAGS['EOD'].replace('%', '%%'))  # escape % chars

    # Read user's template file
    else:
        if PathMaster().is_url(config['template']):
            template = Readfile(config['template'], remove_linebreaks=1)
        else:
            templatefile = ''
            names = [config['template'] + '.' + target, config['template']]
            for filename in names:
                if os.path.isfile(filename):
                    templatefile = filename
                    break
            if not templatefile:
                Error(_("Cannot find template file:") + ' ' + config['template'])
            template = Readfile(templatefile, remove_linebreaks=1)

    head_data = {'STYLE': [], 'ENCODING': ''}

    # Fix CSS files path
    config['stylepath_out'] = fix_css_out_path(config)

    # Populate head_data with config info
    for key in head_data.keys():
        val = config.get(key.lower())
        if key == 'STYLE' and 'html' in target:
            val = config.get('stylepath_out') or []
        # Remove .sty extension from each style filename (freaking tex)
        # XXX Can't handle --style foo.sty, bar.sty
        if target in ['tex', 'texs'] and key == 'STYLE':
            val = map(lambda x: re.sub('(?i)\.sty$', '', x), val)
        if key == 'ENCODING':
            val = get_encoding_string(val, target)
        head_data[key] = val

    # Parse header contents
    for i in 0, 1, 2:
        # Expand macros
        contents = MacroMaster(config=config).expand(headers[i])
        # Escapes - on tex, just do it if any \tag{} present
        if target not in ['tex', 'texs'] or \
          (target in ['tex', 'texs'] and re.search(r'\\\w+{', contents)):
            contents = doEscape(target, contents)
        if target == 'lout':
            contents = doFinalEscape(target, contents)

        head_data['HEADER%d' % (i + 1)] = contents

    # When using --css-inside, the template's <STYLE> line must be removed.
    # Template line removal for empty header keys is made some lines above.
    # That's why we will clean STYLE now.
    if target in ('html', 'xhtml', 'xhtmls', 'html5', 'htmls', 'wp') and config.get('css-inside') and config.get('style'):
        head_data['STYLE'] = []

    Debug("Header Data: %s" % head_data, 1)

    # ASCII Art and rst don't use a header template, aa.header() formats the header
    if rules['asciiart'] and not (rules['spread'] and not rules['web']):
        template = aa.header(head_data, AA, config['width'], config['height'], rules['web'], rules['slides'])
        if rules['slides']:
            l = aa.lencjk(head_data['HEADER2']) + aa.lencjk(head_data['HEADER3']) + 2
            bar_1 = bar_2 = aa.line(AA['bar2'], config['width'])
            if rules['web']:
                bar_1 = '<section><pre>' + bar_1
            n_page = 0
            for i, line in enumerate(config['fullBody']):
                if config['fullBody'][i - 1] == bar_1 and config['fullBody'][i + 3] == bar_2:
                    n_page += 1
            page = 1
            for i, line in enumerate(config['fullBody']):
                if config['fullBody'][i - 1] == bar_1 and config['fullBody'][i + 3] == bar_2:
                    pages = str(page) + '/' + str(n_page)
                    l1 = aa.lencjk(head_data['HEADER1']) + len(pages) + 3
                    config['fullBody'][i] = ' ' + head_data['HEADER1'][:config['width'] - len(pages) - 3] + ' ' * (config['width'] - l1) + ' ' + pages + ' '
                    page += 1
                if config['fullBody'][i - 3] == bar_1 and config['fullBody'][i + 1] == bar_2:
                    if l < config['width']:
                        config['fullBody'][i] = ' ' + head_data['HEADER2'] + ' ' * (config['width'] - l) + head_data['HEADER3'] + ' '
        # Header done, let's get out
        if rules['web']:
            encoding = ''
            if CONF['encoding'] and CONF['encoding'] != 'not_utf-8':
                encoding = '<meta charset=' + CONF['encoding'] + '>'
            if rules['spread']:
                pre = '<pre style="text-align:center">'
            elif rules['slides']:
                pre = ''
            else:
                pre = '<pre>'
            head_web = ['<!doctype html><html>' + encoding + '<title>' + config['header1'] + '</title>' + pre]
            foot_web = ['</pre></html>']
            if rules['slides']:
                foot_web = [HEADER_TEMPLATE[target]]
            if rules['spread']:
                return head_web + config['fullBody'] + foot_web
            else:
                return head_web + template + config['fullBody'] + foot_web
        else:
            return template + config['fullBody']

    if target =='rst':
        template =[]
        if head_data['HEADER1']:
            template.extend(aa.under(head_data['HEADER1'], targets.RST['title'], 10000, True))
        if head_data['HEADER2']:
            template.append(':Author: ' + head_data['HEADER2'])
        if head_data['HEADER3']:
            template.append(':Date: ' + head_data['HEADER3'])
        return template + config['fullBody']

    # Scan for empty dictionary keys
    # If found, scan template lines for that key reference
    # If found, remove the reference
    # If there isn't any other key reference on the same line, remove it
    #TODO loop by template line > key
    for key in head_data.keys():
        if head_data.get(key):
            continue
        for line in template:
            if line.count('%%(%s)s' % key):
                sline = line.replace('%%(%s)s' % key, '')
                if not re.search(r'%\([A-Z0-9]+\)s', sline) and not rules['keepblankheaderline']:
                    template.remove(line)

    # Style is a multiple tag.
    # - If none or just one, use default template
    # - If two or more, insert extra lines in a loop (and remove original)
    styles = head_data['STYLE']
    if len(styles) == 1:
        head_data['STYLE'] = styles[0]
    elif len(styles) > 1:
        style_mark = '%(STYLE)s'
        for i in xrange(len(template)):
            if template[i].count(style_mark):
                while styles:
                    template.insert(i + 1, template[i].replace(style_mark, styles.pop()))
                del template[i]
                break

    # Expand macros on *all* lines of the template
    template = map(MacroMaster(config=config).expand, template)
    # Add Body contents to template data
    head_data['BODY'] = '\n'.join(config['fullBody'])
    # Populate template with data (dict expansion)
    template = '\n'.join(template) % head_data

    # Adding CSS contents into template (for --css-inside)
    # This code sux. Dirty++
    if target in ('html', 'xhtml', 'xhtmls', 'html5', 'htmls', 'wp') and config.get('css-inside') and \
       config.get('stylepath'):
        set_global_config(config)  # usually on convert(), needed here
        for i in xrange(len(config['stylepath'])):
            cssfile = config['stylepath'][i]
            try:
                contents = Readfile(cssfile, remove_linebreaks=1)
                css = "\n%s\n%s\n%s\n%s\n" % (
                    doCommentLine("Included %s" % cssfile),
                    TAGS['cssOpen'],
                    '\n'.join(contents),
                    TAGS['cssClose'])
                # Style now is content, needs escaping (tex)
                #css = maskEscapeChar(css)
            except:
                Error(_("CSS include failed for %s") % cssfile)
            # Insert this CSS file contents on the template
            template = re.sub('(?i)(</HEAD>)', css + r'\1', template)
            # template = re.sub(r'(?i)(\\begin{document})',
            #       css + '\n' + r'\1', template)  # tex

        # The last blank line to keep everything separated
        template = re.sub('(?i)(</HEAD>)', '\n' + r'\1', template)

    return template.split('\n')


def doCommentLine(txt):
    # The -- string ends a (h|sg|xht)ml comment :(
    txt = maskEscapeChar(txt)
    if TAGS['comment'].count('--') and txt.count('--'):
        txt = re.sub('-(?=-)', r'-\\', txt)

    if TAGS['comment']:
        return regex['x'].sub(txt, TAGS['comment'])
    return ''


def doFooter(config):
    ret = []

    # No footer. The --no-headers option hides header AND footer
    if not config['headers']:
        return []

    # Only add blank line before footer if last block doesn't added by itself
    if not rules.get('blanksaround' + BLOCK.last):
        ret.append('')

    # Add txt2tags info at footer, if target supports comments
    if TAGS['comment']:

        # Not using TARGET_NAMES because it's i18n'ed.
        # It's best to always present this info in english.
        target = config['target']
        if config['target'] == 'tex':
            target = 'LaTeX2e'
        if rules['asciiart']:
            target = 'ASCII Art'

        t2t_version = '%s code generated by %s %s (%s)' % (target, my_name, my_version, my_url)
        cmdline = 'cmdline: %s %s' % (my_name, ' '.join(config['realcmdline']))

        ret.append(doCommentLine(t2t_version))
        ret.append(doCommentLine(cmdline))

    # Maybe we have a specific tag to close the document?
    #if TAGS['EOD']:
    #   ret.append(TAGS['EOD'])

    return ret


#this converts proper \ue37f escapes to RTF \u-7297 escapes
def convertUnicodeRTF(match):
    num = int(match.group(1), 16)
    if num > 32767:
        num = num | -65536
    return ESCCHAR + 'u' + str(num) + '?'


def get_escapes(target):
    if target == 'texs':
        target = 'tex'
    return ESCAPES.get(target, [])


def doProtect(target, txt):
    "Protect text in tagged blocks from being escaped."
    for before, protected, after in get_escapes(target):
        txt = txt.replace(before, protected)
    return txt


def doEscape(target, txt):
    "Target-specific special escapes. Apply *before* insert any tag."
    tmpmask = 'vvvvThisEscapingSuxvvvv'

    if rules['escapexmlchars']:
        txt = re.sub('&', '&amp;', txt)
        txt = re.sub('<', '&lt;', txt)
        txt = re.sub('>', '&gt;', txt)

    if target == 'sgml':
            txt = re.sub('\xff', '&yuml;', txt)  # "+y
    elif target == 'pm6':
        txt = re.sub('<', '<\#60>', txt)
    elif target == 'mgp':
        txt = re.sub('^%', ' %', txt)  # add leading blank to avoid parse
    elif target == 'man':
        txt = re.sub("^([.'])", '\\&\\1', txt)              # command ID
        txt = txt.replace(ESCCHAR, ESCCHAR + 'e')           # \e
    elif target == 'lout':
        # TIP: / moved to FinalEscape to avoid //italic//
        # TIP: these are also converted by lout:  ...  ---  --
        txt = txt.replace(ESCCHAR, tmpmask)                 # \
        txt = txt.replace('"', '"%s""' % ESCCHAR)           # "\""
        txt = re.sub('([|&{}@#^~])', '"\\1"', txt)          # "@"
        txt = txt.replace(tmpmask, '"%s"' % (ESCCHAR * 2))  # "\\"
    elif target in ['tex', 'texs']:
        # Mark literal \ to be changed to $\backslash$ later
        txt = txt.replace(ESCCHAR, tmpmask)
        txt = re.sub('([#$&%{}])', ESCCHAR + r'\1'  , txt)  # \%
        txt = re.sub('([~^])'    , ESCCHAR + r'\1{}', txt)  # \~{}
        txt = re.sub('([<|>])'   ,           r'$\1$', txt)  # $>$
        txt = txt.replace(tmpmask, maskEscapeChar(r'$\backslash$'))
        # TIP the _ is escaped at the end
    elif target == 'rtf':
        txt = txt.replace(ESCCHAR, ESCCHAR + ESCCHAR)
        txt = re.sub('([{}])', ESCCHAR + r'\1', txt)
        # RTF is ascii only
        # If an encoding is declared, try to convert to RTF unicode
        enc = get_encoding_string(CONF['encoding'], 'rtf')
        if enc:
            try:
                txt = txt.decode(enc)
            except:
                Error('Problem decoding line "' % txt + '"')
            txt = txt.encode('cp1252', 'backslashreplace')
            # escape ANSI codes above ascii range
            for code in range(128, 255):
                txt = re.sub('%c' % code, ESCCHAR + "'" + hex(code)[2:], txt)
            # some code were preescaped by txt.encode
            txt = re.sub(r'\\x([0-9a-f]{2})', r"\\\'\1", txt)
            #finally, convert escaped unicode chars to RTF format
            txt = re.sub(r'\\u([0-9a-f]{4})', convertUnicodeRTF, txt)
    return txt


# TODO man: where - really needs to be escaped?
def doFinalEscape(target, txt):
    "Last escapes of each line"
    for before, protected, after in get_escapes(target):
        # If the string has not been protected, replace it.
        txt = txt.replace(before, after)
        # If the string has been protected, restore it.
        txt = txt.replace(protected, before)
    return txt


def EscapeCharHandler(action, data):
    "Mask/Unmask the Escape Char on the given string"
    if not data.strip():
        return data
    if action not in ('mask', 'unmask'):
        Error("EscapeCharHandler: Invalid action '%s'" % action)
    if action == 'mask':
        return data.replace('\\', ESCCHAR)
    else:
        return data.replace(ESCCHAR, '\\')


def maskEscapeChar(data):
    "Replace any Escape Char \ with a text mask (Input: str or list)"
    if type(data) == type([]):
        return map(lambda x: EscapeCharHandler('mask', x), data)
    return EscapeCharHandler('mask', data)


def unmaskEscapeChar(data):
    "Undo the Escape char \ masking (Input: str or list)"
    if type(data) == type([]):
        return map(lambda x: EscapeCharHandler('unmask', x), data)
    return EscapeCharHandler('unmask', data)


def addLineBreaks(mylist):
    "use LB to respect sys.platform"
    ret = []
    for line in mylist:
        line = line.replace('\n', LB)       # embedded \n's
        ret.append(line + LB)               # add final line break
    return ret


# Convert ['foo\nbar'] to ['foo', 'bar']
def expandLineBreaks(mylist):
    ret = []
    for line in mylist:
        ret.extend(line.split('\n'))
    return ret


def compile_filters(filters, errmsg='Filter'):
    if filters:
        for i in xrange(len(filters)):
            patt, repl = filters[i]
            try:
                rgx = re.compile(patt)
            except:
                Error("%s: '%s'" % (errmsg, patt))
            filters[i] = (rgx, repl)
    return filters


def enclose_me(tagname, txt):
    return TAGS.get(tagname + 'Open') + txt + TAGS.get(tagname + 'Close')


def fix_relative_path(path):
    """
    Fix image/link path to be relative to the source file path (issues 62, 63)

    Leave the path untouched when:
    - not using --fix-path
    - path is an URL (or email)
    - path is an #anchor
    - path is absolute
    - infile is STDIN
    - outfile is STDOUT

    Note: Keep this rules in sync with fix_css_out_path()
    """
    if not CONF['fix-path'] \
        or regex['link'].match(path) \
        or path[0] == '#' \
        or os.path.isabs(path) \
        or CONF['sourcefile'] in [STDIN, MODULEIN] \
        or CONF['outfile'] in [STDOUT, MODULEOUT]:
        return path

    # Make sure the input path is relative to the correct source file.
    # The path may be different from original source file when using %!include
    inputpath = PathMaster().join(os.path.dirname(CONF['currentsourcefile']), path)

    # Now adjust the inputpath to be reachable from the output folder
    return PathMaster().relpath(inputpath, os.path.dirname(CONF['outfile']))


def fix_css_out_path(config):
    """
    Fix CSS files path to be reached from the output folder (issue 71)

    Needed when the output file is in a different folder than the sources.
    This will affect the HTML's <link rel="stylesheet"> header tag.

    Leave the path untouched when:
    - not using --fix-path
    - path is an URL
    - path is absolute
    - infile is STDIN
    - outfile is STDOUT

    Note: Keep this rules in sync with fix_relative_path()
    """

    # No CSS files
    if not config.get('style'):
        return None

    # Defaults to user-typed paths
    default = config['style'][:]

    if not config['fix-path'] \
        or config['sourcefile'] in [STDIN, MODULEIN] \
        or config['outfile'] in [STDOUT, MODULEOUT]:
        return default

    # Sanity
    if len(config['style']) != len(config['stylepath']):
        Error("stylepath corrupted. Sorry, this shouldn't happen :(")

    # The stylepath paths are relative to the INPUT file folder.
    # Now we must make them relative to the OUTPUT file folder.
    stylepath_out = []
    for (userpath, fixedpath) in zip(config['style'], config['stylepath']):
        if os.path.isabs(userpath):
            # Never fix user-typed absolute paths
            path = userpath
        else:
            path = PathMaster().relpath(fixedpath, os.path.dirname(config['outfile']))
        stylepath_out.append(path)
    return stylepath_out


def beautify_me(name, font, line):
    "where name is: bold, italic, underline or strike"

    # Exception: Doesn't parse an horizontal bar as strike
    if name == 'strike' and regex['bar'].search(line):
        return line

    open_ = TAGS['%sOpen' % font]
    close = TAGS['%sClose' % font]
    txt = r'%s\1%s' % (open_, close)
    line = regex[font].sub(txt, line)
    return line


def get_tagged_link(label, url):
    ret = ''
    target = CONF['target']
    image_re = regex['img']

    # Set link type
    if regex['email'].match(url):
        linktype = 'email'
    else:
        linktype = 'url'

    # Escape specials from TEXT parts
    label = doEscape(target, label)

    # Escape specials from link URL
    if not rules['linkable'] or rules['escapeurl']:
        url = doEscape(target, url)

    # Adding protocol to guessed link
    guessurl = ''
    if linktype == 'url' and \
       re.match('(?i)' + regex['_urlskel']['guess'], url):
        if url[0] in 'Ww':
            guessurl = 'http://' + url
        else:
            guessurl = 'ftp://' + url

        # Not link aware targets -> protocol is useless
        if not rules['linkable']:
            guessurl = ''

    # Simple link (not guessed)
    if not label and not guessurl:
        if CONF['mask-email'] and linktype == 'email':
            # Do the email mask feature (no TAGs, just text)
            url = url.replace('@', ' (a) ')
            url = url.replace('.', ' ')
            url = "<%s>" % url
            if rules['linkable']:
                url = doEscape(target, url)
            ret = url
        else:
            # Just add link data to tag
            tag = TAGS[linktype]
            ret = regex['x'].sub(url, tag)

    # Named link or guessed simple link
    else:
        # Adjusts for guessed link
        if not label:
            label = url         # no protocol
        if guessurl:
            url = guessurl      # with protocol

        # Image inside link!
        if image_re.match(label):
            if rules['imglinkable']:  # get image tag
                label = parse_images(label)
            else:                     # img@link !supported
                img_path = image_re.match(label).group(1)
                label = "(%s)" % fix_relative_path(img_path)

        if rules['asciiart'] and not rules['slides'] and not rules['web'] and not rules['spread'] and not CONF['toc-only']:
            for macro in MASK.macrobank:
                macro = MASK.macroman.expand(macro)
                url = url.replace(MASK.macromask, macro, 1)
            if url not in AA_MARKS:
                AA_MARKS.append(url)
            url = str(AA_MARKS.index(url) + 1)

        # Putting data on the right appearance order
        if rules['labelbeforelink'] or not rules['linkable']:
            urlorder = [label, url]   # label before link
        else:
            urlorder = [url, label]   # link before label

        ret = TAGS["%sMark" % linktype]

        # Exception: tag for anchor link is different from the link tag
        if url.startswith('#') and TAGS['urlMarkAnchor']:
            ret = TAGS['urlMarkAnchor']

        # Add link data to tag (replace \a's)
        for data in urlorder:
            ret = regex['x'].sub(data, ret, 1)

        if TARGET == 'rst' and '.. image::' in label:
            ret = label[:-2] + TAGS['urlImg'] + url + label[-2:]

    return ret


def parse_deflist_term(line):
    "Extract and parse definition list term contents"
    img_re = regex['img']
    term   = regex['deflist'].search(line).group(3)

    # Mask image inside term as (image.jpg), where not supported
    if not rules['imgasdefterm'] and img_re.search(term):
        while img_re.search(term):
            imgfile = img_re.search(term).group(1)
            term = img_re.sub('(%s)' % imgfile, term, 1)

    #TODO tex: escape ] on term. \], \rbrack{} and \verb!]! don't work :(
    return term


def get_image_align(line):
    "Return the image (first found) align for the given line"

    # First clear marks that can mess align detection
    line = re.sub(SEPARATOR + '$', '', line)  # remove deflist sep
    line = re.sub('^' + SEPARATOR, '', line)  # remove list sep
    line = re.sub('^[\t]+'       , '', line)  # remove quote mark

    # Get image position on the line
    m = regex['img'].search(line)
    ini = m.start()
    head = 0
    end = m.end()
    tail = len(line)

    # The align detection algorithm
    if   ini == head and end != tail:
        align = 'left'      # ^img + text$
    elif ini != head and end == tail:
        align = 'right'     # ^text + img$
    else:
        align = 'center'    # default align

    # Some special cases
    if BLOCK.isblock('table'):
        align = 'center'    # ignore when table
#   if TARGET == 'mgp' and align == 'center': align = 'center'

    return align


# Reference: http://www.iana.org/assignments/character-sets
# http://www.drclue.net/F1.cgi/HTML/META/META.html
def get_encoding_string(enc, target):
    if not enc:
        return ''
    if target == 'texs':
        target = 'tex'
    # Target specific translation table
    translate = {
        'tex': {
            # missing: ansinew , applemac , cp437 , cp437de , cp865
            'utf-8'       : 'utf8',
            'us-ascii'    : 'ascii',
            'windows-1250': 'cp1250',
            'windows-1252': 'cp1252',
            'ibm850'      : 'cp850',
            'ibm852'      : 'cp852',
            'iso-8859-1'  : 'latin1',
            'iso-8859-2'  : 'latin2',
            'iso-8859-3'  : 'latin3',
            'iso-8859-4'  : 'latin4',
            'iso-8859-5'  : 'latin5',
            'iso-8859-9'  : 'latin9',
            'koi8-r'      : 'koi8-r'
        },
        'rtf': {
            'utf-8'       : 'utf8',
        }
    }
    # Normalization
    enc = re.sub('(?i)(us[-_]?)?ascii|us|ibm367', 'us-ascii'  , enc)
    enc = re.sub('(?i)(ibm|cp)?85([02])'        , 'ibm85\\2'  , enc)
    enc = re.sub('(?i)(iso[_-]?)?8859[_-]?'     , 'iso-8859-' , enc)
    enc = re.sub('iso-8859-($|[^1-9]).*'        , 'iso-8859-1', enc)
    # Apply translation table
    try:
        enc = translate[target][enc.lower()]
    except:
        pass
    return enc


##############################################################################
##MerryChristmas,IdontwanttofighttonightwithyouImissyourbodyandIneedyourlove##
##############################################################################


def process_source_file(file_='', noconf=0, contents=[], gui_raw=[]):
    """
    Find and Join all the configuration available for a source file.
    No sanity checking is done on this step.
    It also extracts the source document parts into separate holders.

    The config scan order is:
        1. The user configuration file (i.e. $HOME/.txt2tagsrc)
        2. The source document's CONF area
        3. The command line options

    The return data is a tuple of two items:
        1. The parsed config dictionary
        2. The document's parts, as a (head, conf, body) tuple

    All the conversion process will be based on the data and
    configuration returned by this function.
    The source files is read on this step only.
    """
    if contents:
        source = SourceDocument(contents=contents)
    else:
        source = SourceDocument(file_)
    head, conf, body = source.split()
    Message(_("Source document contents stored"), 2)
    if not noconf:
        # Read document config
        source_raw = source.get_raw_config()
        # Join all the config directives found, then parse it
        full_raw = RC_RAW + source_raw + CMDLINE_RAW + gui_raw
        Message(_("Parsing and saving all config found (%03d items)") % (len(full_raw)), 1)
        full_parsed = ConfigMaster(full_raw).parse()
        # Add manually the filename to the conf dic
        if contents:
            full_parsed['sourcefile'] = MODULEIN
            full_parsed['currentsourcefile'] = MODULEIN
            full_parsed['infile'] = MODULEIN
            full_parsed['outfile'] = MODULEOUT
        else:
            full_parsed['sourcefile'] = file_
            full_parsed['currentsourcefile'] = file_
        # Maybe should we dump the config found?
        if full_parsed.get('dump-config'):
            dumpConfig(source_raw, full_parsed)
            Quit()
        # The user just want to know a single config value (hidden feature)
        #TODO pick a better name than --show-config-value
        elif full_parsed.get('show-config-value'):
            config_value = full_parsed.get(full_parsed['show-config-value'])
            if config_value:
                if type(config_value) == type([]):
                    print '\n'.join(config_value)
                else:
                    print config_value
            Quit()
        # Okay, all done
        Debug("FULL config for this file: %s" % full_parsed, 1)
    else:
        full_parsed = {}
    return full_parsed, (head, conf, body)


def get_infiles_config(infiles):
    """
    Find and Join into a single list, all configuration available
    for each input file. This function is supposed to be the very
    first one to be called, before any processing.
    """
    return map(process_source_file, infiles)


def convert_this_files(configs):
    global CONF
    for myconf, doc in configs:                 # multifile support
        target_toc  = []
        target_body = []
        target_foot = []
        source_head, source_conf, source_body = doc
        myconf = ConfigMaster().sanity(myconf)

        if myconf['target'] in ['aat', 'txt', 'rst', 'mgp'] and myconf['encoding'].lower() == 'utf-8':
            decode_head, decode_body = [], []
            try:
                for line in source_head:
                    decode_head.append(line.decode('utf-8'))
                for line in source_body:
                    decode_body.append(line.decode('utf-8'))
                source_head, source_body = decode_head, decode_body
            except:
                myconf['encoding'] = 'not_utf-8'
                myconf = ConfigMaster().sanity(myconf)

        # Save header info for %%header1..3 macros
        if not source_head:
            myconf['header1'] = ''
            myconf['header2'] = ''
            myconf['header3'] = ''
        else:
            myconf['header1'] = source_head[0]
            myconf['header2'] = source_head[1]
            myconf['header3'] = source_head[2]

        # Parse the full marked body into tagged target
        first_body_line = (len(source_head) or 1) + len(source_conf) + 1
        Message(_("Composing target Body"), 1)
        target_body, marked_toc = convert(source_body, myconf, firstlinenr=first_body_line)

        # If dump-source, we're done
        if myconf['dump-source']:
            for line in source_head + source_conf + target_body:
                print line
            return

        # Close the last slide
        if rules['slides'] and not myconf['toc-only'] and rules['asciiart']:
            n = (myconf['height'] - 1) - (AA_COUNT % (myconf['height'] - 1) + 1)
            if rules['web']:
                target_body = target_body + ([''] * n) + [aa.line(AA['bar2'], myconf['width']) + '</pre></section>']
            else:
                target_body = target_body + ([''] * n) + [aa.line(AA['bar2'], myconf['width'])]
            if myconf['qa']:
                n_before = (myconf['height'] - 24) / 2
                n_after = myconf['height'] - 24 - n_before
                head = aa.slide(_("Q&A"), AA['bar2'], myconf['width'], rules['web'])
                end_qa = aa.line(AA['bar2'], myconf['width'])
                if rules['web']:
                    end_qa = end_qa + '</pre></section>'
                if myconf['height'] > 23 and myconf['width'] > 22:
                    target_body = target_body + head + [''] * n_before + [(myconf['width'] - 23) / 2 * ' ' + line for line in aa.QA] + [''] * n_after + [end_qa]
                else:
                    target_body = target_body + head + [''] * (myconf['height'] - 7) + [end_qa]

        if myconf['target'] == 'mgp' and myconf['qa']:
                target_body = target_body + ['%page', '', 'Q&A', '', '%center', '%newimage "templates/QA.png"', '']

        if rules['asciiart'] and not rules['slides'] and not rules['web'] and not rules['spread'] and not myconf['toc-only']:
            for i, url in enumerate(AA_MARKS):
                target_body.extend(aa.wrap('[' + str(i + 1) + '] ' + url, myconf['width'], rules['web']))

        # Compose the target file Footer
        Message(_("Composing target Footer"), 1)
        target_foot = doFooter(myconf)

        # Make TOC (if needed)
        Message(_("Composing target TOC"), 1)
        tagged_toc  = toc_tagger(marked_toc, myconf)
        target_toc  = toc_formatter(tagged_toc, myconf)
        target_body = toc_inside_body(target_body, target_toc, myconf)
        if not AUTOTOC and not myconf['toc-only']:
            target_toc = []
        # Finally, we have our document
        myconf['fullBody'] = target_toc + target_body + target_foot

        # Compose the target file Headers
        #TODO escape line before?
        #TODO see exceptions by tex and mgp
        Message(_("Composing target Headers"), 1)
        outlist = doHeader(source_head, myconf)

        if rules['asciiart'] and rules['web'] and not myconf['headers']:
            outlist = ['<pre>'] + outlist + ['</pre>']

        # If on GUI, abort before finish_him
        # If module, return finish_him as list
        # Else, write results to file or STDOUT
        if GUI:
            return outlist, myconf
        elif myconf.get('outfile') == MODULEOUT:
            return finish_him(outlist, myconf), myconf
        else:
            Message(_("Saving results to the output file"), 1)
            finish_him(outlist, myconf)


def getImageInfo(filename):
    "Get image type, dimensions, and pixel size."
    try:
        f = open(filename, 'rb')
        head = f.read(2)
        # Default DPI (if none specified in image metadata) of 72
        dpix = 72
        dpiy = 72
        if head == '\x89\x50':  # PNG format image
            imgtype = 'png'
            magic, length, chunkid, width, height, bit_depth, colour_type = struct.unpack('!6sI4sIIBBxxxxxxx', f.read(31))
            if (magic == '\x4e\x47\x0d\x0a\x1a\x0a') and \
                    (length > 0) and \
                    (chunkid == 'IHDR'):
                        chunk = f.read(8)
                        # Now to find the DPI / Pixel dimensions
                        while chunk:
                            length, chunkid = struct.unpack('!I4s', chunk)
                            if chunkid == 'pHYs':
                                dpix, dpiy, units = struct.unpack('!IIbxxxx', f.read(13))
                                if units == 1:
                                    # PNG images have pixel dimensions in pixels per meter,
                                    # convert to pixels per inch
                                    dpix = dpix * 0.0257
                                    dpiy = dpiy * 0.0257
                                else:
                                    # No pixel dimensions, set back to default
                                    dpix = 72
                                    dpiy = 72
                            elif chunkid == 'IDAT':
                                data = f.read(length)
                                f.seek(4, 1)
                            else:
                                f.seek(length + 4, 1)
                            chunk = f.read(8)
                        f.close()
                        return imgtype, width, height, bit_depth, colour_type, dpix, dpiy, data
            else:
                f.close()
                Error('Cannot embed PNG image ' + filename + '. Badly formatted.')

        elif head == '\xff\xd8':  # JPG format image
            imgtype = 'jpeg'
            # Jpeg format is insane. The image size chunk could be anywhere,
            # so we need to search the whole file.
            b = f.read(1)
            while (b != ''):
                # Each chunk in a jpeg file is delimited by at least one
                # \xff character, and possibly more for padding. Seek past 'em
                while (b != '\xff'):
                    b = f.read(1)
                while (b == '\xff'):
                    b = f.read(1)

                # Past them, now to find the type of this particular chunk
                if b == '\xe0':
                    # Header, should be the first chunk in the file.
                    size = struct.unpack('!H', f.read(2))
                    if f.read(5) == 'JFIF\0':
                        # This Jpeg has JFIF metadata, which should include pixel dimensions
                        units, dpix, dpiy = struct.unpack('!xxbHH', f.read(7))
                        if units == 0:
                            # No exact pixel dimensions, just return defaults
                            dpix = 72
                            dpiy = 72
                        elif units == 2:
                            # Pixel dimensions in pixels per centimeter, so convert.
                            #  units == 1 would mean the field is in pixels per inch,
                            #  so no conversion needed in that case.
                            dpix = dpix * 2.57
                            dpiy = dpiy * 2.57
                        f.seek(size[0] - 12, 1)
                    else:
                        # No metadata, just keep the default 72 dpi and
                        # find the image size.
                        f.seek(size[0] - 7, 1)
                    b = f.read(1)
                elif (b >= '\xc0') and (b <= '\xc3'):
                    # Image info chunk, which should include size in pixels
                    height, width = struct.unpack('!xxxHH', f.read(7))
                    f.close()
                    return imgtype, width, height, 'bit_depth', 'colour_type', dpix, dpiy, 'data'

                else:
                    # Wrong chunk type. Get length of chunk and skip to the next one
                    size = struct.unpack('!H', f.read(2))
                    f.seek(size[0] - 2, 1)
                    b = f.read(1)
            f.close()
            # No size information found
            Error('Cannot embed JPG image ' + filename + '. Badly formatted.')
        else:  # Not a supported image format
            f.close()
            Error('Cannot embed image ' + filename + '. Unsupported format.')
    except:
        Error('Cannot embed image ' + filename + '. Unable to open file.')

RTFIMGID = 1000  # Needed so each embedded image can have a unique ID number


def embedImage(filename):
    mytype, width, height, bit_depth, colour_type, dpix, dpiy, data = getImageInfo(filename)
    if TARGET in ('html','xhtml','xhtmls','html5', 'htmls'):

        ## return a data uri with the image embed.
        ## see: http://en.wikipedia.org/wiki/Data_URI_scheme

        line = "data: image/%s;base64,"%mytype
        line = line+base64.b64encode(file(filename).read())
        return line

    elif TARGET == 'rtf':
        global RTFIMGID
        RTFIMGID += 1
        # Defalt DPI of images.
        if dpix == 0 and dpiy == 0:
            dpix = 72
            dpiy = 72
        try:
            filein = open(filename, 'rb')
            # RTF tags for an embedded bitmap image, with size in pixels and intended display size in twips.
            # Size and dpi converted to float for division, as by default Python 2 will return an integer,
            # probably truncated to 0 in most cases. This behavior is changed in Python3.
            line = r'\\%sblip\\picw%d\\pich%d\\picwgoal%d\\picscalex100\\pichgoal%d\\picscaley100\\bliptag%d{\\*\\blipuid%016x}' \
                    % (mytype, width, height, int(float(width) / float(dpix) * 1440.0), int(float(height) / float(dpiy) * 1440.0), RTFIMGID, RTFIMGID)
            line = line + filein.read().encode('hex')
            filein.close()
            return line
        except:
            Error('Unable to embed image: ' + filename)

    elif rules['asciiart']:
        if mytype not in ['png']:
            Error("Cannot embed image " + filename + ". Unsupported " + mytype + " format with Ascii Art targets. You should use PNG.")
        if colour_type == 3:
            Error("Cannot embed image " + filename + ". Unsupported indexed-colour image type with Ascii Art targets. You should use greyscale or RGB.")
        if bit_depth not in [8]:
            Error("Cannot embed image " + filename + ". Unsupported bit depth with Ascii Art targets. You should use 8-bit pixels.")
        import zlib
        decomp = zlib.decompress(data)
        n_byte = n_byte_alpha = (colour_type % 4 + 1)
        if colour_type in [4, 6]:
            n_byte_alpha = n_byte + 1
        image = []
        end_line = n_byte_alpha * width + 1
        while decomp:
            line = decomp[1:end_line]
            line_img = []
            while line:
                if n_byte == 1:
                    L, = struct.unpack('!B', line[:n_byte])
                else:
                    R, G, B = struct.unpack('!BBB', line[:n_byte])
                    # ITU-R 601-2 luma transform
                    L = int(0.299 * R + 0.587 * G + 0.114 * B)
                line_img.append(L)
                line = line[n_byte_alpha:]
            image.append(line_img)
            decomp = decomp[end_line:]
        return aa.image(image)


def parse_images(line):
    "Tag all images found"
    global CONF
    while regex['img'].search(line):
        txt = regex['img'].search(line).group(1)
        tag = TAGS['img']

        txt = fix_relative_path(txt)

        # If target supports image alignment, here we go
        if rules['imgalignable']:

            align = get_image_align(line)         # right
            align_name = align.capitalize()       # Right

            # The align is a full tag, or part of the image tag (~A~)
            if TAGS['imgAlign' + align_name]:
                tag = TAGS['imgAlign' + align_name]
            else:
                align_tag = TAGS['_imgAlign' + align_name]
                tag = regex['_imgAlign'].sub(align_tag, tag, 1)

            # Dirty fix to allow centered solo images
            if align == 'center' and TARGET in ('html', 'xhtml'):
                rest = regex['img'].sub('', line, 1)
                if re.match('^\s+$', rest):
                    tag = "<center>%s</center>" % tag
            if align == 'center' and TARGET == 'xhtmls':
                rest = regex['img'].sub('', line, 1)
                if re.match('^\s+$', rest):
                    ## original (not validating):
                    # tag = '<div style="text-align: center;">%s</div>' % tag
                    ## dirty fix:
                    # tag = '</p><div style="text-align: center;">%s</div><p>' % tag
                    ## will validate, though img won't be centered:
                    tag = '%s' % tag

        # Rtf needs some tweaking
        if TARGET == 'rtf' and not CONF.get('embed-images'):
            # insert './' for relative paths if needed
            if not re.match(r':/|:\\', txt):
                tag = regex['x'].sub('./\a', tag, 1)
            # insert image filename an extra time for readers that don't grok linked images
            tag = regex['x'].sub(txt, tag, 1)

        if TARGET in  ['tex', 'texs']:
            tag = re.sub(r'\\b', r'\\\\b', tag)
            txt = txt.replace('_', 'vvvvTexUndervvvv')

        if CONF.get('embed-images'):
            # Embedded images find files from the same location as linked images,
            # for consistant behaviour.
            basedir = os.path.dirname(CONF.get('outfile'))
            fullpath = PathMaster().join(basedir, txt)
            txt = embedImage(fullpath)
            if rules['asciiart']:
                return txt

        if rules['asciiart'] and rules['slides'] and rules['web']:
            global AA_IMG
            mytype, width, height, bit_depth, colour_type, dpix, dpiy, data = getImageInfo(txt)
            AA_IMG = int((height / 600.0) * (CONF['height'] - 1))

        # Ugly hack to avoid infinite loop when target's image tag contains []
        tag = tag.replace('[', 'vvvvEscapeSquareBracketvvvv')

        line = regex['img'].sub(tag, line, 1)
        line = regex['x'].sub(txt, line, 1)

        if TARGET == 'rst':
            line = line.split('ENDIMG')[0] + line.split('ENDIMG')[1].strip()

    return line.replace('vvvvEscapeSquareBracketvvvv', '[')


def add_inline_tags(line):
    # We can't use beauti.capitalize() for the beautifiers, because
    # 'i'.capitalize != 'I' for turkish locales.
    for beauti, font in [('bold', 'fontBold'), ('italic', 'fontItalic'),
                          ('underline', 'fontUnderline'), ('strike', 'fontStrike')]:
        if regex[font].search(line):
            line = beautify_me(beauti, font, line)

    line = parse_images(line)
    return line


def get_include_contents(file_, path=''):
    "Parses %!include: value and extract file contents"
    ids = {'`': 'verb', '"': 'raw', "'": 'tagged'}
    id_ = 't2t'
    # Set include type and remove identifier marks
    mark = file_[0]
    if mark in ids:
        if file_[:2] == file_[-2:] == mark * 2:
            id_ = ids[mark]      # set type
            file_ = file_[2:-2]  # remove marks
    # Handle remote dir execution
    filepath = PathMaster().join(path, file_)
    # Read included file contents
    lines = Readfile(filepath, remove_linebreaks=1)
    # Default txt2tags marked text, just BODY matters
    if id_ == 't2t':
        lines = get_file_body(filepath)
        lines.insert(0, '%%!currentfile: %s' % (filepath))
        # This appears when included hit EOF with verbatim area open
        #lines.append('%%INCLUDED(%s) ends here: %s' % (id_, file_))
    return id_, lines


def set_global_config(config):
    global CONF, TAGS, regex, rules, TARGET
    CONF   = config
    rules  = getRules(CONF)

    # ASCII Art slides need width and height
    if rules['slides'] and rules['asciiart']:
        if rules['web']:
            config['height'], config['width'] = 28, 73
        if not config['width'] and  not config['height'] and os.name == 'posix':
            import fcntl, termios
            data = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, 4*'00')
            config['height'], config['width'] = struct.unpack('4H',data)[:2]
        if not config['width']:
            config['width'] = DFT_SLIDE_WIDTH
        if not config['height']:
            config['height'] = DFT_SLIDE_HEIGHT
    # Wrapped targets need width
    if rules['iswrapped'] and not config['width']:
        config['width'] = DFT_TEXT_WIDTH
    if rules['asciiart'] and config['width'] < 5:
        Error(_("--width: Expected width > 4, got %i") % config['width'])
    targets.CONF['width'] = config['width']

    TAGS   = getTags(CONF)
    regex  = getRegexes()
    TARGET = config['target']  # save for buggy functions that need global


def convert(bodylines, config, firstlinenr=1):
    global BLOCK, TITLE, MASK

    set_global_config(config)

    target = config['target']
    BLOCK = BlockMaster()
    MASK  =  MaskMaster()
    TITLE = TitleMaster()

    ret = []
    dump_source = []
    f_lastwasblank = 0

    # Compiling all PreProc regexes
    pre_filter = compile_filters(
        CONF['preproc'], _('Invalid PreProc filter regex'))

    # Let's mark it up!
    linenr = firstlinenr - 1
    lineref = 0
    while lineref < len(bodylines):
        # Defaults
        results_box = ''

        untouchedline = bodylines[lineref]
        dump_source.append(untouchedline)

        line = re.sub('[\n\r]+$', '', untouchedline)   # del line break

        # Apply PreProc filters
        if pre_filter:
            errmsg = _('Invalid PreProc filter replacement')
            for rgx, repl in pre_filter:
                try:
                    line = rgx.sub(repl, line)
                except:
                    Error("%s: '%s'" % (errmsg, repl))

        line = maskEscapeChar(line)                  # protect \ char
        linenr  += 1
        lineref += 1

        Debug(repr(line), 2, linenr)  # heavy debug: show each line

        #------------------[ Comment Block ]------------------------

        # We're already on a comment block
        if BLOCK.block() == 'comment':

            # Closing comment
            if regex['blockCommentClose'].search(line):
                ret.extend(BLOCK.blockout() or [])
                continue

            # Normal comment-inside line. Ignore it.
            continue

        # Detecting comment block init
        if regex['blockCommentOpen'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('comment'))
            continue

        #-------------------------[ Tagged Text ]----------------------

        # We're already on a tagged block
        if BLOCK.block() == 'tagged':

            # Closing tagged
            if regex['blockTaggedClose'].search(line):
                ret.extend(BLOCK.blockout())
                continue

            # Normal tagged-inside line
            BLOCK.holdadd(line)
            continue

        # Detecting tagged block init
        if regex['blockTaggedOpen'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('tagged'))
            continue

        # One line tagged text
        if regex['1lineTagged'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('tagged'))
            line = regex['1lineTagged'].sub('', line)
            BLOCK.holdadd(line)
            ret.extend(BLOCK.blockout())
            continue

        #-------------------------[ Raw Text ]----------------------

        # We're already on a raw block
        if BLOCK.block() == 'raw':

            # Closing raw
            if regex['blockRawClose'].search(line):
                ret.extend(BLOCK.blockout())
                continue

            # Normal raw-inside line
            BLOCK.holdadd(line)
            continue

        # Detecting raw block init
        if regex['blockRawOpen'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('raw'))
            continue

        # One line raw text
        if regex['1lineRaw'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('raw'))
            line = regex['1lineRaw'].sub('', line)
            BLOCK.holdadd(line)
            ret.extend(BLOCK.blockout())
            continue

        #------------------------[ Verbatim  ]----------------------

        #TIP We'll never support beautifiers inside verbatim

        # Closing table mapped to verb
        if BLOCK.block() == 'verb' \
           and BLOCK.prop('mapped') == 'table' \
           and not regex['table'].search(line):
            ret.extend(BLOCK.blockout())

        # We're already on a verb block
        if BLOCK.block() == 'verb':

            # Closing verb
            if regex['blockVerbClose'].search(line):
                ret.extend(BLOCK.blockout())
                continue

            # Normal verb-inside line
            BLOCK.holdadd(line)
            continue

        # Detecting verb block init
        if regex['blockVerbOpen'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('verb'))
            f_lastwasblank = 0
            continue

        # One line verb-formatted text
        if regex['1lineVerb'].search(line) \
           and BLOCK.block() not in BLOCK.exclusive:
            ret.extend(BLOCK.blockin('verb'))
            line = regex['1lineVerb'].sub('', line)
            BLOCK.holdadd(line)
            ret.extend(BLOCK.blockout())
            f_lastwasblank = 0
            continue

        # Tables are mapped to verb when target is not table-aware
        if not rules['tableable'] and regex['table'].search(line):
            if not BLOCK.isblock('verb'):
                ret.extend(BLOCK.blockin('verb'))
                BLOCK.propset('mapped', 'table')
                BLOCK.holdadd(line)
                continue

        #---------------------[ blank lines ]-----------------------

        if regex['blankline'].search(line):

            # Close open paragraph
            if BLOCK.isblock('para'):
                ret.extend(BLOCK.blockout())
                f_lastwasblank = 1
                continue

            # Close all open tables
            if BLOCK.isblock('table'):
                ret.extend(BLOCK.blockout())
                f_lastwasblank = 1
                continue

            # Close all open quotes
            while BLOCK.isblock('quote'):
                ret.extend(BLOCK.blockout())

            # Closing all open lists
            if f_lastwasblank:          # 2nd consecutive blank
                if BLOCK.block().endswith('list'):
                    BLOCK.holdaddsub('')   # helps parser
                while BLOCK.depth:  # closes list (if any)
                    ret.extend(BLOCK.blockout())
                continue            # ignore consecutive blanks

            # Paragraph (if any) is wanted inside lists also
            if BLOCK.block().endswith('list'):
                BLOCK.holdaddsub('')

            f_lastwasblank = 1
            continue

        #---------------------[ special ]---------------------------

        if regex['special'].search(line):

            targ, key, val = ConfigLines().parse_line(line, None, target)
            global MAILING

            if key:
                Debug("Found config '%s', value '%s'" % (key, val), 1, linenr)
            else:
                Debug('Bogus Special Line', 1, linenr)

            # %!include command
            if key == 'include':

                # The current path is always relative to the file where %!include appeared
                incfile = val
                incpath = os.path.dirname(CONF['currentsourcefile'])
                fullpath = PathMaster().join(incpath, incfile)

                # Infinite loop detection
                if os.path.abspath(fullpath) == os.path.abspath(CONF['currentsourcefile']):
                    Error("%s: %s" % (_('A file cannot include itself (loop!)'), fullpath))

                inctype, inclines = get_include_contents(incfile, incpath)

                # Verb, raw and tagged are easy
                if inctype != 't2t':
                    ret.extend(BLOCK.blockin(inctype))
                    BLOCK.holdextend(inclines)
                    ret.extend(BLOCK.blockout())
                else:
                    # Insert include lines into body
                    #TODO include maxdepth limit
                    bodylines = bodylines[:lineref] + inclines + ['%%!currentfile: %s' % (CONF['currentsourcefile'])] + bodylines[lineref:]
                    # Remove %!include call
                    if CONF['dump-source']:
                        dump_source.pop()

            # %!currentfile command
            elif key == 'currentfile':
                targ, key, val = ConfigLines().parse_line(line, 'currentfile', target)
                if key:
                    Debug("Found config '%s', value '%s'" % (key, val), 1, linenr)
                    CONF['currentsourcefile'] = val
                # This line is done, go to next
                continue

            # %!fen command
            # Forsyth-Edward Notation
            elif key == 'fen':

                # Handle options and arguments
                fenopt = FenOptions(val)
                filename = fenopt.get('infile')
                fen_unicode = fenopt.get('unicode')

                if fen_unicode:
                    if CONF['encoding'].lower() != 'utf-8':
                        if not config['encoding']:
                            Error(_("%!fen: Expected an UTF-8 file to use unicode fen option, you could set %!encoding: UTF-8"))
                        else: 
                            Error(_("%!fen: Expected an UTF-8 file to use unicode fen option"))
                    unicode_chars = [unichr(char) for char in range(0x2654,0x2660)]
                    uni = dict(zip('KQRBNPkqrbnp', unicode_chars))

                for line in Readfile(filename):
                    board = line.split()[0]
                    for i in range(1, 9):
                        board = board.replace(str(i), i * ' ')
                    if fen_unicode:
                        for piece in uni:
                            board = board.replace(piece, uni[piece])
                    board = board.split('/')
                    table = convert_to_table(board, False, True, True)
                    ret.extend(parse_convert_table(table, rules['tableable'], CONF['target']))
                # This line is done, go to next
                continue
            
            # %!db command
            elif key == 'db':

                try:
                    import sqlite3
                except:
                    Error('No sqlite3 module')

                # Handle options and arguments
                dbopt = DbOptions(val)
                filename = dbopt.get('infile')
                db_query = dbopt.get('query')
                db_borders = dbopt.get('borders')
                db_center = dbopt.get('center')
                db_headers = dbopt.get('headers')
                db_mailing = dbopt.get('mailing')
                if db_mailing:           
                    db_headers = True
                
                sqlite3.register_converter('NULL', unicode)
                sqlite3.register_converter('INTEGER', unicode)
                sqlite3.register_converter('REAL', unicode)
                sqlite3.register_converter('BLOB', unicode)

                db = sqlite3.connect(filename, detect_types=sqlite3.PARSE_DECLTYPES)
                dbc = db.cursor()

                if db_query:
                    res = dbc.execute(db_query)
                    if db_headers:
                        result = [[[header[0] for header in dbc.description]] + res.fetchall()]
                    else:
                        result = [res.fetchall()]
                else:
                    result = []
                    table_names = dbc.execute("select name from sqlite_master where type='table'").fetchall()
                    for table_name in table_names:
                        res = dbc.execute("select * from " + table_name[0])
                        if db_headers:
                            result.append([[header[0] for header in dbc.description]] + res.fetchall())
                        else:
                            result.append(res.fetchall())

                db.close()

                if not db_mailing:
                    for tab in result:
                        # Convert each DB line to a txt2tags' table line
                        table = convert_to_table(tab, db_headers, db_borders, db_center)
                        # Parse and convert the new table
                        ret.extend(parse_convert_table(table, rules['tableable'], CONF['target']))
                else:
                    if not MAILING and not len(result) > 1:
                        MAILING = result[0]
                    else:
                        Error("Mailing: a document can't be linked to more than one db")

                # This line is done, go to next
                continue

            # %!csv command
            elif key == 'csv':

                # Handle options and arguments
                csvopt = CsvOptions(val)
                filename = csvopt.get('infile')
                csv_separator = csvopt.get('separator')
                csv_quotechar = csvopt.get('quotechar')
                csv_headers = csvopt.get('headers')
                csv_borders = csvopt.get('borders')
                csv_center = csvopt.get('center')
                csv_utf8 = csvopt.get('utf8')
                csv_mailing = csvopt.get('mailing')

                if not filename:
                    Error(_('%!CSV command: Missing CSV file path:') + ' ' + val)

                if csv_separator == 'space':
                    csv_separator = ' '
                elif csv_separator == 'tab':
                    csv_separator = '\t'

                if csv_quotechar:
                    reader = csv.reader(Readfile(filename), delimiter=csv_separator, quotechar=csv_quotechar, quoting=csv.QUOTE_MINIMAL)
                else:
                    reader = csv.reader(Readfile(filename), delimiter=csv_separator, quoting=csv.QUOTE_NONE)

                if csv_utf8: 
                    reader_utf8 = []
                    for line in reader:
                        line_utf8 = []
                        for el in line:
                            line_utf8.append(el.decode('utf-8'))
                        reader_utf8.append(line_utf8)
                    reader = reader_utf8

                # Convert each CSV line to a txt2tags' table line
                # foo,bar,baz -> | foo | bar | baz |
                if not csv_mailing:
                    try:
                        table = convert_to_table(reader, csv_headers, csv_borders, csv_center)
                    except csv.Error, e:
                        Error('CSV: file %s: %s' % (filename, e))
                    else:
                        # Parse and convert the new table
                        ret.extend(parse_convert_table(table, rules['tableable'], CONF['target']))
                else:
                    if not MAILING:
                        MAILING = reader
                    else:
                        Error("Mailing: a document can't be linked to more than one db")

                # This line is done, go to next
                continue

        #---------------------[ dump-source ]-----------------------

        # We don't need to go any further
        if CONF['dump-source']:
            continue

        #---------------------[ Comments ]--------------------------

        # Just skip them (if not macro)
        if regex['comment'].search(line) and not \
           regex['macros'].match(line) and not \
           regex['toc'].match(line):
            continue

        #---------------------[ Triggers ]--------------------------

        # Valid line, reset blank status
        f_lastwasblank = 0

        # Any NOT quote line closes all open quotes
        if BLOCK.isblock('quote') and not regex['quote'].search(line):
            while BLOCK.isblock('quote'):
                ret.extend(BLOCK.blockout())

        # Any NOT table line closes an open table
        if BLOCK.isblock('table') and not regex['table'].search(line):
            ret.extend(BLOCK.blockout())

        #---------------------[ Horizontal Bar ]--------------------

        if regex['bar'].search(line):

            # Bars inside quotes are handled on the Quote processing
            # Otherwise we parse the bars right here
            #
            if not (BLOCK.isblock('quote') or regex['quote'].search(line)) \
                or (BLOCK.isblock('quote') and not rules['barinsidequote']):

                # Close all the opened blocks
                ret.extend(BLOCK.blockin('bar'))

                # Extract the bar chars (- or =)
                m = regex['bar'].search(line)
                bar_chars = m.group(2)

                # Process and dump the tagged bar
                BLOCK.holdadd(bar_chars)
                ret.extend(BLOCK.blockout())
                Debug("BAR: %s" % line, 6)

                # We're done, nothing more to process
                continue

        #---------------------[ Title ]-----------------------------

        if (regex['title'].search(line) or regex['numtitle'].search(line)) \
            and not BLOCK.block().endswith('list'):

            if regex['title'].search(line):
                name = 'title'
            else:
                name = 'numtitle'

            # Close all the opened blocks
            ret.extend(BLOCK.blockin(name))

            # Process title
            TITLE.add(line)
            ret.extend(BLOCK.blockout())

            # We're done, nothing more to process
            continue

        #---------------------[ %%toc ]-----------------------

        # %%toc line closes paragraph
        if BLOCK.block() == 'para' and regex['toc'].search(line):
            ret.extend(BLOCK.blockout())

        #---------------------[ apply masks ]-----------------------

        line = MASK.mask(line)

        #XXX from here, only block-inside lines will pass

        #---------------------[ Quote ]-----------------------------

        if regex['quote'].search(line):

            # Store number of leading TABS
            quotedepth = len(regex['quote'].search(line).group(0))

            # Don't cross depth limit
            maxdepth = rules['quotemaxdepth']
            if maxdepth and quotedepth > maxdepth:
                quotedepth = maxdepth

            # New quote
            if not BLOCK.isblock('quote'):
                ret.extend(BLOCK.blockin('quote'))

            # New subquotes
            while BLOCK.depth < quotedepth:
                BLOCK.blockin('quote')

            # Closing quotes
            while quotedepth < BLOCK.depth:
                ret.extend(BLOCK.blockout())

            # Bar inside quote
            if regex['bar'].search(line) and rules['barinsidequote']:
                tempBlock = BlockMaster()
                tagged_bar = []
                tagged_bar.extend(tempBlock.blockin('bar'))
                tempBlock.holdadd(line)
                tagged_bar.extend(tempBlock.blockout())
                BLOCK.holdextend(tagged_bar)
                continue

        #---------------------[ Lists ]-----------------------------

        # An empty item also closes the current list
        if BLOCK.block().endswith('list'):
            m = regex['listclose'].match(line)
            if m:
                listindent = m.group(1)
                listtype = m.group(2)
                currlisttype = BLOCK.prop('type')
                currlistindent = BLOCK.prop('indent')
                if listindent == currlistindent and \
                   listtype == currlisttype:
                    ret.extend(BLOCK.blockout())
                    continue

        if   regex['list'].search(line) or \
          regex['numlist'].search(line) or \
          regex['deflist'].search(line):

            listindent = BLOCK.prop('indent')
            listids = ''.join(LISTNAMES.keys())
            m = re.match('^( *)([%s]) ' % listids, line)
            listitemindent = m.group(1)
            listtype = m.group(2)
            listname = LISTNAMES[listtype]
            results_box = BLOCK.holdadd

            # Del list ID (and separate term from definition)
            if listname == 'deflist':
                term = parse_deflist_term(line)
                line = regex['deflist'].sub(
                    SEPARATOR + term + SEPARATOR, line)
            else:
                line = regex[listname].sub(SEPARATOR, line)

            # Don't cross depth limit
            maxdepth = rules['listmaxdepth']
            if maxdepth and BLOCK.depth == maxdepth:
                if len(listitemindent) > len(listindent):
                    listitemindent = listindent

            # List bumping (same indent, diff mark)
            # Close the currently open list to clear the mess
            if BLOCK.block().endswith('list') \
               and listname != BLOCK.block() \
               and len(listitemindent) == len(listindent):
                ret.extend(BLOCK.blockout())
                listindent = BLOCK.prop('indent')

            # Open mother list or sublist
            if not BLOCK.block().endswith('list') or \
               len(listitemindent) > len(listindent):
                ret.extend(BLOCK.blockin(listname))
                BLOCK.propset('indent', listitemindent)
                BLOCK.propset('type', listtype)

            # Closing sublists
            while len(listitemindent) < len(BLOCK.prop('indent')):
                ret.extend(BLOCK.blockout())

            # O-oh, sublist before list ("\n\n  - foo\n- foo")
            # Fix: close sublist (as mother), open another list
            if not BLOCK.block().endswith('list'):
                ret.extend(BLOCK.blockin(listname))
                BLOCK.propset('indent', listitemindent)
                BLOCK.propset('type', listtype)

        #---------------------[ Table ]-----------------------------

        #TODO escape undesired format inside table
        #TODO add pm6 target
        if regex['table'].search(line):

            if not BLOCK.isblock('table'):   # first table line!
                ret.extend(BLOCK.blockin('table'))
                BLOCK.tableparser.__init__(line)

            tablerow = TableMaster().parse_row(line)
            BLOCK.tableparser.add_row(tablerow)     # save config

            # Maintain line to unmask and inlines
            # XXX Bug: | **bo | ld** | turns **bo\x01ld** and gets converted :(
            # TODO isolate unmask+inlines parsing to use here
            line = SEPARATOR.join(tablerow['cells'])

        #---------------------[ Paragraph ]-------------------------

        if not BLOCK.block() and \
           not line.count(MASK.tocmask):  # new para!
            ret.extend(BLOCK.blockin('para'))

        ############################################################
        ############################################################
        ############################################################

        #---------------------[ Final Parses ]----------------------

        # The target-specific special char escapes for body lines
        line = doEscape(target, line)

        line = add_inline_tags(line)
        line = MASK.undo(line)

        #---------------------[ Hold or Return? ]-------------------

        ### Now we must choose where to put the parsed line
        #
        if not results_box:
            # List item extra lines
            if BLOCK.block().endswith('list'):
                results_box = BLOCK.holdaddsub
            # Other blocks
            elif BLOCK.block():
                results_box = BLOCK.holdadd
            # No blocks
            else:
                line = doFinalEscape(target, line)
                results_box = ret.append

        results_box(line)

    # EOF: close any open para/verb/lists/table/quotes
    Debug('EOF', 7)
    while BLOCK.block():
        ret.extend(BLOCK.blockout())

    # Maybe close some opened title area?
    if rules['titleblocks']:
        ret.extend(TITLE.close_all())

    # Maybe a major tag to enclose body? (like DIV for CSS)
    if TAGS['bodyOpen']:
        ret.insert(0, TAGS['bodyOpen'])
    if TAGS['bodyClose']:
        ret.append(TAGS['bodyClose'])

    if CONF['toc-only']:
        ret = []
    marked_toc = TITLE.dump_marked_toc(CONF['toc-level'])

    # If dump-source, all parsing is ignored
    if CONF['dump-source']:
        ret = dump_source[:]

    return ret, marked_toc


##############################################################################

def exec_command_line(user_cmdline=[]):
    global CMDLINE_RAW, RC_RAW, DEBUG, VERBOSE, QUIET, GUI, Error

    # Extract command line data
    cmdline_data = user_cmdline or sys.argv[1:]
    CMDLINE_RAW = CommandLine().get_raw_config(cmdline_data, relative=1)
    cmdline_parsed = ConfigMaster(CMDLINE_RAW).parse()
    DEBUG   = cmdline_parsed.get('debug') or 0
    VERBOSE = cmdline_parsed.get('verbose') or 0
    QUIET   = cmdline_parsed.get('quiet') or 0
    GUI     = cmdline_parsed.get('gui') or 0
    infiles = cmdline_parsed.get('infile') or []

    Message(_("Txt2tags %s processing begins") % my_version, 1)

    # The easy ones
    if cmdline_parsed.get('help'):
        Quit(Usage())
    if cmdline_parsed.get('version'):
        Quit(VERSIONSTR)
    if cmdline_parsed.get('targets'):
        listTargets()
        Quit()

    # Multifile haters
    if len(infiles) > 1:
        errmsg = _("Option --%s can't be used with multiple input files")
        for option in NO_MULTI_INPUT:
            if cmdline_parsed.get(option):
                Error(errmsg % option)

    Debug("system platform: %s" % sys.platform)
    Debug("python version: %s" % (sys.version.split('(')[0]))
    Debug("line break char: %s" % repr(LB))
    Debug("command line: %s" % sys.argv)
    Debug("command line raw config: %s" % CMDLINE_RAW, 1)

    # Extract RC file config
    if cmdline_parsed.get('rc') == 0:
        Message(_("Ignoring user configuration file"), 1)
    else:
        rc_file = get_rc_path()
        if os.path.isfile(rc_file):
            Message(_("Loading user configuration file"), 1)
            RC_RAW = ConfigLines(file_=rc_file).get_raw_config()

        Debug("rc file: %s" % rc_file)
        Debug("rc file raw config: %s" % RC_RAW, 1)

    # Get all infiles config (if any)
    infiles_config = get_infiles_config(infiles)

    # Is GUI available?
    # Try to load and start GUI interface for --gui
    if GUI:
        try:
            from lib import gui_tk
        except:
            Error(_('gui_tk module not found, so GUI is not available, use CLI instead.'))
        try:
            winbox = gui_tk.Gui(globals())
            Debug("GUI display OK")
            GUI = 1
        except:
            Debug("GUI Error: no DISPLAY")
            GUI = 0

    # User forced --gui, but it's not available
    if cmdline_parsed.get('gui') and not GUI:
        print getTraceback()
        print
        Error(
            "Sorry, I can't run my Graphical Interface - GUI\n"
            "- Check if Python Tcl/Tk module is installed (Tkinter)\n"
            "- Make sure you are in a graphical environment (like X)")

    # Okay, we will use GUI
    if GUI:
        Message(_("We are on GUI interface"), 1)

        # Redefine Error function to raise exception instead sys.exit()
        def Error(msg):
            gui_tk.tkMessageBox.showerror(_('txt2tags ERROR!'), msg)
            raise error

        # If no input file, get RC+cmdline config, else full config
        if not infiles:
            gui_conf = ConfigMaster(RC_RAW + CMDLINE_RAW).parse()
        else:
            try:
                gui_conf = infiles_config[0][0]
            except:
                gui_conf = {}

        # Sanity is needed to set outfile and other things
        gui_conf = ConfigMaster().sanity(gui_conf, gui=1)
        Debug("GUI config: %s" % gui_conf, 5)

        # Insert config and populate the nice window!
        winbox.load_config(gui_conf)
        winbox.mainwindow()

    # Console mode rocks forever!
    else:
        Message(_("We are on Command Line interface"), 1)

        # Called with no arguments, show error
        # TODO#1: this checking should be only in ConfigMaster.sanity()
        if not infiles:
            Error(_('Missing input file (try --help)') + '\n\n' +
            _('Please inform an input file (.t2t) at the end of the command.') + '\n' +
            _('Example:') + ' %s -t html %s' % (my_name, _('file.t2t')))

        convert_this_files(infiles_config)

    Message(_("Txt2tags finished successfully"), 1)

if __name__ == '__main__':
    try:
        exec_command_line()
    except error, msg:
        sys.stderr.write("%s\n" % msg)
        sys.stderr.flush()
        sys.exit(1)
    except SystemExit:
        pass
    except:
        sys.stderr.write(getUnknownErrorMessage())
        sys.stderr.flush()
        sys.exit(1)
    Quit()

# The End.
